1개의 server process, 1개 이상의 client process
1. 클라이언트가 서비스 필요로 할 때 클라이언트는 1개의 요청(request)를 서버에 보내는 것으로 트랜잭션 개시
(예를 들어 웹 브라우저가 파일을 필요로 할 때 웹 서버로 요청 보냄)
2. 서버는 요청을 받고 해석하고 자신의 자원들을 조작. (예를 들어 웹 서버가 브라우저로부터 요청을 받을 때 디스크 파일 읽음)
3. 서버는 응답(Response)를 클라이언트로 보내고, 그 후 다음 요청 기다림.
4. 클라이언트는 응답을 받고 이를 처리.
: 연결의 종단점. 네트워크 통신을 위한 엔드 포인트
각 소켓은 인터넷 주소와 16 비트 정수 비트로 이루어진 소켓 주소
를 가짐. address : port
-> 클라이언트 소켓 주소 내의 포트는 -> 클라이언트가 연결 요청할 때 커널이 자동으로 할당.(단기 ephemeral port
).
서버 소켓 주소에 있는 포트는 대개 영구적으로 이 서비스에 연결되는 잘 알려진 포트
=> 웹 서버: 포트 80, 이메일 서버: 포트 25
소켓 쌍
: cliaddr: cliport, servaddr, servport)
소켓 주소 구조체
: 일반적인 형태의 소켓 주소 제공
sa_family
: 프로토콜 패밀리(주소 체계) -> AF_INET = IPv4 프로토콜, AF_INET6 = IPv6 프로토콜다양한 주소 구조체 처리를 위해 struc sockaddr가 사용도미 -> 함수에 구체적인 주소 구조체 전달할 때는 그 주소 구조체 자체를 struct sockaddr *로 형변환하여 전달하게 됨
IPv4
특정 소켓 주소 구조체 - 인터넷 기반 통신 설정할 때 사용.
sin_family
: 주소 체계
sin_port
;: 포트 번호를 네트워크 바이트 순서로 나타냄. 소켓이 연결을 듣거나, 연결을 시작할 특정 포트를 지정
sin_addr
: IP 주소를 네트워크 바이트 순서로 나타냄. 이 필드는 실제 IPv4 주소를 포함
sin_zero[8]
: struct sockaddr_in 구조체의 크기를 struct sockaddr와 동일하게 맞추기 위한 패딩. 주로 0으로 초기화
헷갈렸던 점
: Client Socket과 Connection이 만들어지는 소켓은 Server Socket이 아니라 accept API 내부에서 새로 만들어지는 소켓임 (여기서는connfd
)
실질적인 데이터 송수신은 서버listenfd
가 아니라connfd
로 생각하면 된다!
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
listening descriptor
: 클라이언트 연결 요청의 종착점.
서버가 클라이언트의 연결 요청을 기다리도록 설정된 소켓,
서버의 수명 동안 한번 생성되어 지속적으로 존재, 서버가 실행되는 동안 듣고 있음.
클라이언트로부터 들어오는 연결 요청을 감지하고 수락하는 역할connected descriptor
: 클라이언트와 서버 간의 실제 연결의 종착점
서버가 클라이언트의 연결 요청을 수락할 때마다 새로 생성.
이 디스크립터를 통해 클라이언트와 서버 간의 데이터 송수신이 이루어짐
클라이언트를 서비스를 하는 동안만 존재하며 작업이 완료되면 소멸됨.
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 실제 내부 버퍼 - 현재 8192 바이트(데이터를 임시로 저장하는 공간으로 사용)*/ } rio_t;
=> 일반적인 Read, write 시스템 콜은 전체 요청된 바이트 수를 읽거나 쓰지 못할 수 있음.
입출력 장치를 견고하게 만들어준다는 의미
RIO는 2가지 형태를 띠고 있음.
1)
2진 데이터로 이루어진 Unbuffered input / output
-> rio_readn, rio_writen 함수로 이를 처리할 것
2)2진 데이터와 텍스트로 이루어진 Buffered input
-> rio_readlineb, rio_readnb 함수로 처리할 것
ssize_t rio_readn(int fd, void *usrbuf, size_t n)
ssize_t rio_writen(int fd, void *usrbuf, size_t n)
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
void rio_readinitb(rio_t *rp, int fd)
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n)
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
rio_readn
: 식별자 fd의 현재 파일 위치에서 메모리 위치 usrbuf로 최대 n바이트 전송.
- read가 -1을 리턴하는 경우
- 파일을 끝까지 전부 읽었을 때
를 제외하고는 저장할 버퍼 크기만큼 read하는 것을 보장- Signal에 의해 읽기가 중단되지 않음 -> call read() again
rio_writen
: usrbuf에서 식별자 fd로 n바이트 전송
- 언제나 버퍼 크기 만큼 쓰기 동작 수행하려고 함
- Signal에 의해 쓰기 중단 되지 않음 -> call write() again
RIO에서 버퍼를 관리
rio_buf
는 버퍼가 시작된 지점
rio_bufptr
은 현재 읽는 부분을 나타내는 포인터,
rio_cnt
는 읽을 부분의 수
코드를 입력하세요
rio_readinitb: rio_t 타입의 구조체 초기화 함수
rio_readlineb: 한 줄 단위로 입력 받음
rio_t 구조체, 저장할 버퍼, 그리고 읽어들일 최대 바이트 크기를 인자로 받음. 함수는 개행 문자('\n')를 만나거나 지정된 최대 바이트 크기에 도달할 때까지 데이터를 읽음.
rio_readnb: 파일 fd에서 n바이트 크기를 읽어옴
rio_t 구조체, 저장할 버퍼, 그리고 읽어들일 바이트 수를 인자로 받음. 함수는 지정된 바이트 수만큼 데이터를 읽어 버퍼에 저장
int open_clientfd(char *hostname, char *port)
: 호스트 hostname에서 돌아가고 포트번호 port에 연결 요청을 듣는 서버와 연결을 설정
-> 입력과 출력에 대해 준비된 열린 소켓 식별자를 리턴
int open_clientfd(char *hostname, char *port) {
int clientfd, rc;
struct addrinfo hints, *listp, *p;
/* 잠재적인 서버 주소 목록 가져옴*/
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Open a TCP connection, 연결을 위한 소켓 타입 지정 */
hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. 숫자 포트 인자 사용 */
hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections, 연결에 권장되는 플래그 설정 */
if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
return -2; //getaddrinfo 오류 시 -2 반환
}
//연결 가능한 주소 찾아서 순회
//Socket()과 connect()성공할 때까지 linked list 탐색
/* Walk the list for one that we can successfully connect to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor 소켓 디스크립터 생성 */
if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next, 소켓 생성 실패, 다음 주소 시도 */
/* Connect to the server. Socket() 성공 후 서버에 연결 시도*/
if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break; /* Success 성공 시 반복 중단*/
if (close(clientfd) < 0) { /* Connect failed, try another, 연결 실패 다른 주소 시도 */
fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
return -1; //오류시 -1 반환
}
}
/* Clean up */
freeaddrinfo(listp);
if (!p) /* All connects failed -> -1 리턴*/
return -1;
else /* The last connect succeeded */
return clientfd;
}
int open_listenfd(char *port)
{
struct addrinfo hints, *listp, *p;
int listenfd, rc, optval=1;
/* Get a list of potential server addresses 잠재적인 서버 주소 목록 가져옴*/
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Accept connections 연결 수락*/
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
return -2;
}
//socket()과 bind()가 성공할 떄까지 링크드리스트 탐색
//바인딩할 수 있는 주소를 리스트에서 찾음
/* Walk the list for one that we can bind to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor, 소켓 디스크립터 생성*/
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next, 소켓 생성 실패, 다음 주소로 시도 */
//주소가 이미 사용중입니다 오류 방지 - SO_REUSEADDR => 주소 재사용 가능하게 함
/* Eliminates "Address already in use" error from bind */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, //line:netp:csapp:setsockopt
(const void *)&optval , sizeof(int));
//bind를 활용하여 포트번호를 ip주소에 묶음
/* Bind the descriptor to the address */
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break; /* Success 성공, */
if (close(listenfd) < 0) { /* Bind failed, try the next 바인드 실패 -> 다음 주소로 시도*/
fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
return -1;
}
}
/* Clean up 정리 작업*/
freeaddrinfo(listp);
if (!p) /* No address worked 모든 주소 시도 실패*/
return -1;
/* Make it a listening socket ready to accept connection requests */
// 듣기 소켓을 준비하여 연결 요청을 수락할 준비
if (listen(listenfd, LISTENQ) < 0) {
close(listenfd);
return -1;
}
return listenfd;
}
//0번째 인자 : 실행 파일, 1번쨰 인자: host name, 2번째 : 포트 번호
int main(int argc, char **argv) {
int clientfd; //클라이언트 소켓 파일 디스크립터
char *host, *port, buf[MAXLINE]; //호스트, 포트, 데이터 버퍼
rio_t rio; //Robust I/O 구조체
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 연결
Rio_readinitb(&rio, clientfd);
//표준 입력에서 텍스트 줄을 반복적으로 읽음
//표준 입력 Stdin에서 MAXLINE만큼 바이트를 가져와 buf에 저장
//fgets가 EOF 표준 입력을 만나면 종료 (사용자가 Ctrl + D를 눌렀거나 파일로 텍스트 줄을 모두 소진)
while (Fgets(buf, MAXLINE, stdin) != NULL) {
Rio_writen(clientfd, buf, strlen(buf)); //사용자 입력을 서버에 전송
Rio_readlineb(&rio, buf, MAXLINE); //서버로부터 응답을 받음
Fputs(buf, stdout); //받은 응답을 표준 출력으로 출력
}
Close(clientfd); //클라이언트 소켓을 닫음
exit(0); //클라이언트 종료
}
void echo(int connfd);
int main(int argc, char **argv) {
int listenfd, connfd; //서버의 리스닝 소켓, 연결 소켓 파일 디스크립터
socklen_t clientlen; //클라이언트 주소의 크기
struct sockaddr_storage clientaddr; //클라이언트 주소 정보를 저장할 구조체
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);
}
//클라이언트로부터 데이터를 받아 동일한
void echo(int connfd) {
size_t n;
char buf[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, connfd);
//클라이언트로부터 한줄 씩 읽기 - 반환값이 0이면 클라이언트가 연결을 닫았음을 의미
//loop는 클라이언트 연결이 닫힐 때까지 계속
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
printf("server received %d bytes\n", (int)n); //서버가 받은 데이터의 바이트 수 출력
Rio_writen(connfd, buf, n); //받은 데이터를 그대로 다시 보내기
}
}
잘못된 부분이 있다면 댓글로 알려주세요,,,