Echo 클라이언트는 서버와의 연결을 수립한 이후에 클라이언트는 표준 입력에서 텍스트 줄을 반복해서 읽는 루프에 진입하고, 서버에 텍스트 줄을 전송, 서버에서 echo 줄을 읽어서 그 결과를 표준 출력으로 인쇄한다.
Echo의 메인 함수는 아래와 같다.
#include "csapp.h"
int main(int argc, char **argv)
{
int clientfd;
char *host, *port, buf[MAXLINE];
rio_t rio;
if (argc != 3)
{
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(0);
}
host = argv[1];
port = argv[2];
clientfd = Open_clientfd(host, port);
Rio_readinitb(&rio, clientfd);
while (Fgets(buf, MAXLINE, stdin) != NULL)
{
Rio_writen(clientfd, buf, strlen(buf));
Rio_readlineb(&rio, buf, MAXLINE);
Fputs(buf, stdout);
}
Close(clientfd);
exit(0);
}
9~17 줄 : argument를 제대로 받아왔다면, 사용자의 입력은 프로그램이름_host명_port 가 될 것이다. 이를 host와 port 변수에 저장한다. 그리고 이 host와 port로 clientfd를 생성한다.
18 줄 : clientfd로 rio_t 구조체를 초기화한다.Rio_readinitb의 함수의 상세는 아래와 같다. (Robust I/O package :: 돼지 (tistory.com) 참조)
/* rio_t 구조체 초기화 */
void rio_readinitb(rio_t *rp, int fd)
{
rp->rio_fd = fd;
rp->rio_cnt = 0; /* unread size 라고는 하는데 사용하는 걸 보면 read size 라고 봐야 된다.... */
rp->rio_bufptr = rp->rio_buf; /* 내부 버퍼 포인터 */
}
20~24 줄 : 클라이언트가 서버로 텍스트 줄을 전송하면, 서버에서 echo 줄을 읽어서 그 결과를 표준 출력으로 인쇄한다. 루프는 fgets가 EOF(End of File) 표준 입력을 만나면 종료한다. (그러니깐 입력이 더 들어오지 않으면)
22 줄 : Rio_writen을 실행한다. (rio_writen은 buf에서 식별자 fd로 n바이트를 전송한다.) Rio_writen 의 상세는 다음과 같다.
ssize_t rio_writen(int fd, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nwritten;
char *bufp = usrbuf;
while (nleft > 0) {
if ((nwritten = write(fd, bufp, nleft)) <= 0) {
if (errno == EINTR) /* interrupted by sig handler return */
nwritten = 0; /* and call write() again */
else
return -1; /* errorno set by write() */
}
nleft -= nwritten;
bufp += nwritten;
}
return n;
}
23 줄 : Rio_readlineb를 실행한다. (텍스트 줄을 파일 rio에서 읽고, 이것을 메모리 위치 buf로 복사하고, 텍스트라인을 널(0) 문자로 종료시킨다. ( read/write 에서 fd를 인자로 받아 사용했다면, rio_t라는 별도의 구조체가 존재한다. 이 구조체에는 fd정보는 물론 내부적인 임시 버퍼와 관련된 정보들도 포함되어 있다.)
24 줄 : buf가 가지고 있는 문자열을 stdout에 쓴다. (표준 출력으로 표시한다.)
26줄 : 클라이언트의 프로세스가 종료할 때, 커널이 fd를 모두 닫아주기 때문에 따로 close할 필요는 없으나, 임의로 만든 것은 자기 손으로 닫는 것이 올바른 프로그래밍 습관이다. (라고 책이 말한다.)
※ Rio 구조체 ( [개발 일지] rio_t 구조체 :: 돼지 (tistory.com) 참조 )
Rio 구조체는 파일 디스크립터 뿐 아니라 내부 임시 버퍼와 관련된 정보가 포함되어 있다. 상세는 아레와 같다.
/* $begin rio_t */
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd; /* Descriptor for this internal buf */
int rio_cnt; /* Unread bytes in internal buf */
char *rio_bufptr; /* Next unread byte in internal buf */
char rio_buf[RIO_BUFSIZE]; /* Internal buffer */
} rio_t;
/* $end rio_t */
int rio_fd : 파일 디스크립터 정보 (client_fd나 server_fd가 여기에 들어간다.)
int rio_cnt : 아직 읽지않은 버퍼의 바이트 수 ( 0이 되면 버퍼르 다 읽어서 비었다고 판단한다.)
rio_bufptr : 읽을 위치를 표시하는 포인터
rio_buf[RIO_BUFSIZE] : 내부 버퍼 (문자열을 보관하는 array)
/* 지금의 추측으로는 이렇다.
1. 서버와 통신이 시작된 뒤, 우리가 입력을 넣으면 (네트워크용) 버퍼에 저장된다.(fgets())
2. 버퍼에 저장된 데이터를 fd (소켓은 일종의 통로라고 생각하면)로 상대방 버퍼에 넘긴다.
(그러면 서버는 이 데이터를 받고 무엇가 작업을 한 뒤, 결과 텍스트를 넘겨줄 것이다. 그리고 fd를 통해서 서버로부터 온 데이터들이 클라이언트 쪽 버퍼에 저장된다. )
3. 버퍼에 저장되어있는, 서버로부터 온 데이터를 읽는다.
4. 데이터를 출력한다.
RIO는 네트워크를 위해서 쓰는 I/O 형식이라고 하는데 웹서버를 이해하기 위해선 공부해야될 것 같다. */
그 다음은 반복적 서버 메인 루틴이다.
#include "csapp.h"
void echo(int connfd);
int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr; /* Enough space for any address */
char client_hostname[MAXLINE], client_port[MAXLINE];
if (argc != 2)
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
listenfd = Open_listenfd(argv[1]);
while (1)
{
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *)&clientaddr, clientlen, client_hostname, MAXLINE,
client_port, MAXLINE, 0);
printf("Connected to (%s, %s)\n", client_hostname, client_port);
echo(connfd);
Close(connfd);
}
exit(0);
}
서버는 listenfd를 연 뒤에 connect 요청이 올 때까지 무한 반복 루프를 시행한다.
'프로그래밍 공부 > CSPP' 카테고리의 다른 글
11.4 소켓 인터페이스 (작성 중) (0) | 2021.02.01 |
---|---|
[C언어, 웹서버] open_clientfd / listen_clientfd (0) | 2021.01.24 |
[C언어, 웹서버] HostInfo (0) | 2021.01.24 |
[C언어, 웹서버] client 관련 함수 (0) | 2021.01.24 |
[C언어, 웹서버] server 관련 함수 및 소스코드 (0) | 2021.01.24 |
댓글