4장에서는 TCP 서버/클라이언트를 구축하는데 필요한 기본 소켓 함수들을 다루고 있다. 4장에서 기본 소켓 함수들을 예시를 들어서 설명 후, 5장에서 TCP 서버/클라이언트를 점진적으로 개선한다.
어떤 프로토콜을 이용해서 통신을 하고 싶은지 명시하면서 socket() 호출
#include <sys/socket.h>
int socket(int family, int type, int protocol);
return: 성공시 음수아닌 file descriptor, 실패시 -1
protocol | description |
---|---|
IPPROTO_TCP | TCP transport protocol |
IPPROTO_UDP | UDP transport protocol |
IPPROTO_SCTP | SCTP transport protocol |
AF로 시작하는 상수와 PF 로 시작하는 상수는 <sys/socket.h>헤더파일에서 포함하는 <bits/socket.h> 헤더파일에서 정의되어 있다.
여기에서 알 수 있듯이, AP 와 PF 가 동일한 값을 가지긴 하지만, 둘이 항상 같다는 보장은 없다.
ex) AF_LOCAL이 3으로 정의되어 있고, PF_LOCAL이 5로 정의되어 있는데, AF_LOCAL과 PF_LOCAL이 같을 것이라는 생각에 코드를 짰다가 엄청나게 많은 코드들이 깨질 수도 있다.
그렇기 때문에, socket 함수의 인자에는 PF 상수를 넘겨주고, 소켓 주소 구조체에는 AP 상수를 집어넣는 것이 바람직하다.
AF는 address family를 나타내고 PF는 protocol family를 나타냄
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
return: 성공시 0, 오류시 -1
sockfd: socket()을 호출하고 나서 반환되는 socket descriptor
servaddr: 소켓 주소 구조체를 가리키는 포인터
addrlen: 소켓 주소 구조체의 크기
서버에 연결요청해야하기 때문에, 소켓 주소 구조체에는 서버의 IP주소와 포트번호가 들어가야한다.
클라이언트는 서버가 bind 를 호출하기도 전에 connect 를 호출하면 안 된다.
connect()는 연결이 설정되었을때나 에러가 발생했을 떄만 반환된다.
TCP 클라이언트가 SYN 세그먼트에 대한 응답을 받지 못했을 경우, errno 에 ETIMEDOUT 가 세팅되면서 반환
클라이언트의 SYN 세그먼트에 대한 서버의 응답이 리셋(RST)을 의미하는 경우
이는 명시된 서버의 특정 포트로 연결을 기다리는 프로세스가 없다는 것을 의미
이런 경우 hard error로 불리기도 하고, RST세그먼트 받자마자 errno에 ECONNREFUSED 세팅되면서 반환
RST
TCP 연결이 뭔가 잘못되었을때 전송되는 TCP 세그먼트 타입
RST 세그먼트를 보내게 되는 경우
1. port 를 listening 하고 있는 서버가 없는데, 해당 포트에 대해 연결하고자 하는 SYN 세그먼트가 도착한 경우
2. 연결을 중단하고자 하는 경우
3. 존재하지 않는 연결에 대한 세그먼트를 받은 경우
ENETUNREACH
호스트가 속한 네트워크를 찾지 못하는 경우에 발생
EHOSTUNREACH
호스트가 속한 네트워크는 찾았지만, 호스트에 문제가 있는 경우 발생
ex) 주간 서버를 실행하는 로컬 호스트 지정후 출력확인
$ dayitmetcpcli 127.0.0.1
Tue Jan 16 16:45:07 1996
다른 응답형식 보기 위해 HP-UX시스템의 IP주소 지정
$ dayitmetcpcli 206.62.226.62
Tuesday, May 7, 1996 11:01:22-MST
로컬 서브넷 206.62.226에 있지만 호스트 ID(55)에 존재하지 않는 IP주소 지정
$ dayitmetcpcli 206.62.226.55
connect error: Connection timed out
//서브넷에 호스트 ID(55)인 호스트가 없으므로 클라이언트 호스트가 ARP(해당 호스트가 하드웨어 주소로 응답하도록 요청) 요청을 보낼 때 ARP응답을 받지 않습니다.
//연결 시간이 초과 된 후에만 오류가 발생
//err_sys 함수는 ETIMEDOUT 오류 와 관련된 사람이 읽을 수 있는 문자열을 인쇄
주간 서버를 실행하지 않는 호스트(로컬 라우터)를 지정
$ dayitmetcpcli 140.252.1.4
connect error: Connection refused
//서버는 RST로 즉시 응답
인터넷에서 연결할 수 없는 IP 주소를 지정
//tcpdump 로 패킷을 보면 6홉 홉 떨어진 라우터가 ICMP 호스트 도달 불가 오류를 반환한다는 것을 알 수 있습니다
$ dayitmetcpcli 192.3.4.5
connect error: No route to host
//ETIMEDOUT 오류 와 마찬가지로 이 예에서 연결 은 지정된 시간 동안 기다린 후에만 EHOSTUNREACH 오류를 반환
인터넷 프로토콜의 경우, 32비트 IPv4 혹은 128비트 IPv6 주소와 16 비트 TCP/UDP 포트 번호의 조합
#include <sys/socket.h>
int bind(int sockfd , const struct sockaddr * myaddr , socklen_t addrlen );
return: 성공시 0, 실패시 -1
sockfd: socket file descriptor
myaddr: 소켓 주소 구조체에 대한 포인터
addrlen: 소켓 주소 구조체의 크기
서버는 시작할 때 well-known port를 바인딩한다.
TCP client나 server가 이를 수행하지 않으면 커널은 connect() 또는 listen()이 호출될 때 소켓에 대한 임시 포트를 선택한다.
응용프로그램이 reserved port를 요구하지 않는 한 TCP client가 커널이 임시 포트를 선택하도록 하는 것은 정상이지만, TCP server가 커널이 임시 포트를 선택하도록하는 경우는 드물다.
예외)
PRC(원결 프로시저 호출)서버
이 port는 PRC port mapper에 등록되기 때문에 일반적으로 커널이 수신 소켓에 대한 임시 포트를 선택하도록 한다.
client는 server에 연결하기 저넹 임시 포트를 얻기 위해 port mapper에 연락해야한다.
이것은 UDP를 사용하는 RPC 서버에도 적용된다.
프로세스는 특정 IP주소를 소켓에 바인딩할 수 있다.
IP주소는 호스트의 인터페이스에 속해야한다.
TCP client 경우 소켓에서 보낸 IP 데이터그램에 사용할 소스 IP주소 할당
TCP server 경우 소켓이 해당 IP주소로만 들어오는 client 연결을 수신하도록 제한
TCP client는 IP 주소를 소켓에 바인딩 하지 않음
TCP server가 IP 주소를 소켓에 바인딩하지 않으면 커널은 client SYN의 대상 IP 주소를 server의 소스 IP 주소로 사용
포트 번호를 0으로 지정하면 커널은 bind()가 호출될 때 임시 포트를 선택합니다. 그러나 와일드카드 IP 주소를 지정하면 커널은 소켓이 연결되거나(TCP) 소켓에서 데이터그램이 전송될 때까지(UDP) 로컬 IP 주소를 선택하지 않습니다.
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //wildcard
이것은 IP 주소가 간단한 숫자 상수(이 경우 0)로 표시될 수 있는 32비트 값인 IPv4에서 작동하지만 128비트 IPv6 주소가 다음 위치에 저장되기 때문에 IPv6에서는 이 기술을 사용할 수 없다.
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any; //wildcard
INADDR_ANY(0) 의 값은 네트워크 또는 호스트 바이트 순서에서 동일하므로 htonl 을 실제로 사용할 필요는 없다. 그러나 <netinet/in.h> 헤더에 정의된 모든 INADDR_constants 는 호스트 바이트 순서로 정의되므로 이러한 상수와 함께 htonl 을 사용해야함
https://flylib.com/books/en/3.225.1.61/1/
listen 함수는 TCP 서버에서만 호출되며, 크게 두 가지 역할
#include <sys/socket.h>
int listen(int sockfd, int backlog);
backlog 인자를 이해하기 전에, 커널이 listening socket에 대해 두 개의 queue를 관리하는 것을 알고 있어야 한다.
incomplete connection queue
클라이언트로부터 SYN 메시지를 받고, 서버 측에서 3-way handshaking 이 끝나기를 기다리는 소켓에 대한 엔트리를 포함한다. 여기서 각 소켓은 SYN_RCVD 상태이다.
completed connection queue
TCP 3-way handshaking이 완료되어서 연결이 성립된 소켓에 대한 엔트리를 포함한다. 여기서 각 소켓은 ESTABLISHED 상태이다.
불완전한 대기열에 항목이 생성되면 수신 소켓의 매개변수가 새로 생성된 연결로 복사됩니다. 연결 생성 메커니즘은 완전히 자동입니다. 서버 프로세스는 관련되지 않습니다.
큐의 엔트리는 SYN 메시지를 받자마자 incomplete queue에서 처음으로 생성
incomplete queue에 있는 엔트리는 client로 부터 ACK 메시지를 받거나 타임아웃(RTT)이 될 때까지 계속해서 큐에 남게된다.
client로부터 ACK 메시지를 받아서 3-way handshaking이 성공하게 되면 incomplete queue에 있는 엔트리는 completed queue의 맨 마지막 앤트리로 옮겨지게 된다.
backlog 인자는 역사적으로 두 대기열 합계의 최대값을 지정
4.2BSD 메뉴얼 페이지에는 "defines the maximum length the queue of pending connections may grow to"(대기중인 연결 댁열이 커질 수 있는 최대길이를 정의)라고 나와있다.
POSIX 사양에서도 이 정의를 그대로 복사하지만 이 정의는 보류중인 연결이 SYN_RCVD상태인지 아직 수락되지 않은 ESTABLISHED 상태인지 또는 둘중 하나인지 여부를 말하지 않습니다.
1.5곱하기
Berkeley-derived implementations는 backlog에 fudge factor을 추가한다.
ex) 일반적 지정된 backlog는 최대 8개의 대기열에 있는 항목 허용
fudge factor 추가한 이유는 역사에서 사라진것으로 보임
그러나 backlog를 socket에 대해 커널이 대기열에 넣을 최대 연결수를 지정하는것으로 간주하면 퍼지 요인의 이유는 대기열의 불완전한 연결을 고려하기 위한 것
구현에 따라 다르게 해석하므로 백로그로 0지정x
(client가 listening socket에 연결하는것을 원하지 않으면 listening socket을 닫아라)
3-way-handshake 정상적으로 완료된다고 가정하면(손실된 세그먼트 및 재전송 없음) 해당 값이 특정 client와 server간에 발생하더라도 항목은 하나의 RTT(패킷 왕복 시간)에 대해 incomplete connection queue에 남아있다.
문제: 애플리케이션은 백로그 에 대해 어떤 값을 지정해야 할까? 5는 종종 적절하지 않기 때문
이제 HTTP 서버는 더 큰 값을 지정하지만 지정된 값이 소스 코드에서 상수인 경우 상수를 늘리려면 서버를 다시 컴파일해야함
또 다른 방법은 일부 기본값을 가정하지만 명령줄 옵션이나 환경 변수가 기본값을 무시하도록 허용
이 문제에 대한 간단한 해결책은 listen 기능에 대한 wrapper 기능을 수정하여 제공 가능
void Listen(int fd, int backlog){
char *ptr;
//can override 2nd argument with enviroment variable
if((ptr = getenv("LISTENQ")) != NULL)
baklog = atoi(ptr);
if(listen(fd, backlog) < 0)
err_sys("listen error");
}
고정된 수의 연결을 대기열에 넣는 이유는 accept을 위한 연속적인 호출 사이에 서버 프로세스가 사용 중인 경우를 처리하기 위한 것
이는 두 개의 대기열 중 완료된 대기열에 일반적으로 불완전한 대기열보다 더 많은 항목이 있어야 함을 의미
대기열이 가득 차면 RST가 전송되지 않는다.
이는 조건이 일시적인 것으로 간주되고 클라이언트 TCP가 SYN을 재전송하여 가까운 장래에 대기열에서 공간을 찾기 때문
소켓의 수신 버퍼에 대기 중인 데이터
Client가 SYN 패킷만을 계속적으로 보내고 ACK 패킷을 안보내게 되면, Server는 Client의 연결을 받아들이기 위해 RAM(메모리) 공간을 점점 더 많이 확보해둔 상태에서 대기
그리고 Server의 RAM이 꽉 차게 되면 더이상 연결을 받아들일 수 없게되고, Server는 서비스를 서비스 계속할 수 없음
반환된 fd는 클라이언트와의 TCP연결 나타냄
#include <sys/socket.h>
int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
returns: 성공시 커널에서 자동으로 생성한 음이 아닌 새 fd, 실패시 -1
cliaddr 및 addrlen 인수는 연결된 피어 프로세스(클라이언트)의 프로토콜 주소를 반환하는 데 사용
addrlen: value-result 인수
반환 시 이 정수 값은 소켓 주소 구조에서 커널이 저장한 실제 바이트 수를 포함
accept()는 최대 3개의 값 반환
#include "unp.h"
#include <time.h>
int main(int argc, char **argv){
int listenfd, connfd;
socklent_t len;
struct sockaddr_in servaddr, cliaddr;
char buff(MAXLINE);
time_t ticks;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13); /* daytime server */
Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for (;;){
len = sizeof(cliaddr);
//len소켓 주소 구조의 크기로 초기화
connfd = Accept(listenfd, (SA *) &cliaddr, &len);
printf("connection from %s, port %d\n",
//소켓 주소 구조의 32비트 IP 주소를 점분리 십진수 ASCII 문자열로 변환하고 호출
Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
//네트워크 바이트 순서에서 호스트 바이트 순서로 16비트 포트 번호를 변환
ntohs(cliaddr.sin_port));
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
첫 번째 경우에 커널은 소스 IP 주소를 루프백 주소로 설정
두 번째 경우에는 주소를 이더넷 인터페이스의 IP 주소로 설정
Solaris 커널이 선택한 임시 포트가 33188이고 33189임
daytimetcpsrv1.c 에 설명된 서버 는 반복 서버
Unix에서 동시 서버를 작성하는 가장 간단한 방법은 fork각 클라이언트를 처리하는 자식 프로세스
pid_t pid;
int listenfd, connfd;
listenfd = Socket( ... );
/* sockaddr_in{} 을 서버의 well-known port로 채운다.*/
Bind(listenfd, ... );
Listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = Accept (listenfd, ... ); /* probably blocks */
// connfd : 서버와의 3-Way Handshaking에 성공한 클라이언트
// Complete Connection Queue에 있던 SYN을 보낸 클라이언트와의 소켓 중 하나가 connfd에 대응된다.
if( (pid = Fork()) == 0) {
Close(listenfd); /* child closes listening socket */
// Child는 Listen Socket을 사용하지 않는다.
doit(connfd); /* 요청처리 */
Close(connfd); /* done with this client (사실, 다음 구문이 exit()이므로, close는 필요없다.)*/
exit(0); /* child terminates */
}
Close(connfd); /* parent closes connected socket */
// Parent는 Connected Socket을 사용하지 않는다.
}
doit함수가 클라이언트를 서비스하는데 필요한 모든 작업을 수행한다고 가정
소켓이 반환된 후, listenfd와 관련된 파일 테이블 항목은 1의 참조카운트를 가진다.
accept이 반환된 후 connfd와 연결된 파일 테이블 항목은 1의 참조카운트를 가진다.
그러나 fork()가 반환된 후 두 설명자가 모두 공유된다.
따라서 부모가 connfd를 닫으면 참조 카운트가 2에서 1로 감소
descriptor에 대한 실제 닫기는 참조 카운트가 0이될 때까지 발생하지 않음
나중에 자식이 connfd 닫을 때 종료
accept에 대한 호출이 반환되기 전에 accept 호출에서 서버가 차단되고 클라이언트로부터 연결 요청이 도착한다.
accept에서 반환되면 커널이 연결을 수락하고 새 소켓 connfd이 생성된다(이 소켓은 연결된 소켓이며 이제 연결을 통해 데이터를 읽고 쓸 수 있다).
반환 후에 fork는 설명자 listenfd와 connfd, 모두 부모와 자식 간에 공유(복제)된다.
부모가 연결된 소켓을 닫고 자식이 수신 소켓을 닫은 후:
이것은 소켓의 원하는 최종 상태
자식은 클라이언트와의 연결을 처리하고 부모는 accept다음 클라이언트 연결을 처리하기 위해 수신 대기 소켓에서 다시 호출할 수 있다.
TCP는 이미 대기열에 있는 데이터를 다른 쪽 끝으로 보내려고 시도하고, 이것이 발생한 후 정상적인 TCP 연결 종료 시퀀스가 발생
#include <unistd.h>
int close (int sockfd);
close() 호출되면 참조 카운트 감소
TCP 연결에서 FIN을 정말로 보내고 싶다면 대신 shutdown함수를 사용할 수 있다
#include <sys/socket.h>
//socket descriptor할당된 자신의 address(주소)를 얻음
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
//연결된 상대 시스템의 address(주소)를 얻음
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
addrlen: value-result 인수
두 함수 모두 localaddr 또는 peeraddr이 가리키는 소켓 주소 구조를 채운다.
sockaddr: 자신의 주소를 저장할 buffer.
TCP client에서 connect()가 성공적으로 반환된 후, bind를 호출하지 않을 때 getsockname()으로 local IP address와 local port 정보 반환
port number 0으로 호출 후 bind(커널에 local port number선택하도록 지시) getsockname()으로 할당된 local port number 확인
getsockname()주소 패밀리를 얻기 위해 호출 가능
bind wildcard IP address인 TCP서버에서 client와 연결이 설정(accept 성공적 반환)되면 서버는 연결에 할당된 local IP address 얻기 위해 getsockname호출할 수 있다.
(소켓 설명자 인수 getsockname는 수신 소켓이 아니라 연결된 소켓의 인수여야 한다)
서버가 exec 호출하는 프로세스에 의해 수신될 때 서버가 accept클라이언트의 ID를 얻을 수 있는 유일한 방법은 호출하는 것 getpeername
ex)
inetd가 accept호출하면 두개의 value 반환: 함수의 반환 connected socket descriptor(connfd), IP address와 port number 포함하는 "peer's address" 반환
fork로 복제
자식이 실제 서버(예: 우리가 보여주는 텔넷 서버)를 exec하면 하위의 메모리 이미지가 텔넷 서버의 새 프로그램 파일(피어의 주소를 포함하는 소켓 주소 구조가 손실됨)로 교체되고 연결된 소켓 설명자는 실행 전체에서 열린 상태로 유지된다. 텔넷 서버에서 수행하는 첫 번째 함수 호출 중 하나는 클라이언트의 IP 주소와 포트 번호를 얻기 위한 getpeername이다.
이 예에서 Telnet 서버는 시작 시간 값을 알아야 한다.
두 가지 일반적인 방법:
1. 호출하는 프로세스는 이를 exec에 명령줄 인수로 전달합니다
2. accept를 호출하기 전에 특정 descriptor가 항상 연결된 소켓으로 설정된다는 규칙을 설정할 수 있다 - inetd가 하는것, 항상 descriptor 0, 1, 2를 연결된 소켓으로 설정하는 것
int sockfd_to_family(int sockfd){
struct sockaddr_storage ss; //할당된 소켓 주소 구조의 유형을 모르기 때문에 sockaddr_storage 사용(이 값은 시스템에서 지원하는 모든 소켓 주소 구조 보유할 수 있음)
socklen_t len;
len = sizeof(ss); //가장 큰 소켓 주소 구조를 위한 공간을 할당
if (getsockname(sockfd, (SA *) &ss, &len) < 0)
return(-1);
return(ss.ss_family);
}
https://notes.shichao.io/unp/ch4/
https://kodingwarrior.github.io/post/2018/10/19/unix-network-programming-chapter-4.html
https://dad-rock.tistory.com/395?category=839765