
IP 주소 구조체.
struct in_addr {
uint32_t s_addr; // big-endian 순서로 적힌 주소
}
Struct sockaddr_in {
uint16_t sin_family; // 프로토콜 계통. 항상 AF_INET다.
uint16_t sin_port; // 포트 번호.
struct in_addr sin_addr; // IP 주소.
unsigned char sin_zero[8]; // sockaddr 만큼의 패딩.
};
struct sockaddr{
uint16_t sa_family; // 프로토콜 계통.
char sa_data[14]; // 주소 데이터.
}
소켓 식별자 생성.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
성공시 오케이, -1은 에러.
소켓을 끝점으로 만들고 싶다면 이렇게 한다.
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
...
그렇지만,
getaddrinfo 함수로
매개변수를 자동 생성하여 코드가 프로토콜과 무관하게 하는게 가장 좋다.(무관하게 한다고?)
여기까지하면 부분적으로 열렸을뿐,
읽거나 쓸 수는 없다.
소켓을 어떻게 오픈하는지는 클라이언트냐, 서버느냐에 따라 갈린다.
다음은 클라이언트 소켓 오픈 완료를 말한다.
클라이언트는 connect 함수를 호출하여
서버와 연결을 수립한다.
#include <sys/socket.h>
int connect(int clienfd, const struct sockaddr *addr,
socklen_t addrlen);
0리턴시 성공, -1은 실패.
소켓주소 addr 서버와 인터넷 연결을 시도하고
addrlen은 sizeof(sockaddr_in)이 된다.
connect 함수는 연결이 성공할때까지
block 되어있거나, 에러가 발생한다.
성공한다면 clientfd는 이제 읽거나 쓸 준비가 되었으며,
이 연결은 다음과 같은 소켓 쌍으로 규정한다.
(x:y, addr.sin_addr:addr.sin_port)
x는 클라이언트의 IP주소고,
y는 클라이언트 호스트의 클라이언트 프로세스를 유일하게 식별하는 단기 포트다.
가장 좋은 방법은
getaddrinfo로 connect의 인자를 제공하는 것이다!
남아있는 소켓 함수는
서버가 클라이언트와 연결하기 위해 사용한다.
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
0이면 성공, -1이면 실패.
커널에게 addr에 있는 서버의 소켓 주소를
소켓 식별자 sockfd와 연결한다.
addrlen 인자는 sizeof(sockaddr_int)다.
역시 getaddrinfo에서
bind할 인자를 제공하는게 가장 좋다.
서버는 listen 함수를 호출하여
이 식별자를, (클라이언트가 보냈을테니까)
클라이언트 대신 서버가 사용할 거라 알려준다.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
성공시 0, 실패시 -1
sockfd를
능동 소켓에서 듣기 소켓으로 변환하고,
듣기 소켓은 클라이언트로부터 연결 요청을 수락한다.
backlog 인자는
커널이 요청을 거절하기 전까지
큐에 저장가능한 연결 수를 알려준다.
정확한 의미는 TCP/IP 이해라 범주를 벗어난다.
대개 이 값을 1024와 같은 큰 값으로 설정한다.
서버는 accept 함수로
클라이언트로부터의 연결 요청을 기다린다.
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr,
int *addrlen);
양수의 cd 반환시 성공, 실패시 -1.
연결 요청이,
듣기 식별자인 listenfd에 도달하길 기다리고,
그 후에 addr내 클라이언트의 소켓 주소를 채우고,
Unix I/O 함수들로
클라이언트와 통신하기 위해 사용될수 있는 연결 식별자를 리턴한다.
듣기 식별자는 연결 요청에 대한 끝점으로서의 역할이고,
한번 생성되면 서버가 살아있는 동안 계속 존재한다.
연결 식별자는 클라이언트와 서버 사이 연결의 끝점으로,
서버가 연결 요청을 수락할때마다 생성되며,
서버가 클라이언트에 서비스하는 동안에만 존재한다.
...
리눅스는 getaddrinfo와
getnameinfo라는 함수를 제공하는데,
...
소켓 인터페이스와 함께 이용 시
특정 IP 프로토콜 버전에 의존하지 않는 네트워크 프로그램 작성이 가능해진다.
호스트 이름, 호스트 주소,
서비스 이름, 포트 번호의 스트링을
소켓 주소 구조체로 변환해준다.
gethostbyname과 getservbyname의
현대적인 버전이다.
모든 프로토콜에 대해 동작한다.
(재진입 가능? 12.7.2절?)
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host,
const char *service,
const struct addrinfo *hints,
struct addrinfo **result);
// 성공시 0, 0이 아니면 에러.
void freeaddrinfo(struct addrinfo *result);
//리턴하는 것 없음.
const char *gai_strerror(int errcode);
// 에러 메시지 리턴.
struct addrinfo {
int ai_flags; //
int ai_family; // 소켓 기능을 가리키는 arg
int ai_socketype // 두번째 arg
int ai_protocol; // 세번째 arg
char *ai_canonname; //
size_t ai_addrlen; //ai_addr 구조체 크기
struct sockaddr *ai_addr; // 소켓 주소 구조체 포인터
struct addrinfo *ai_next; // 링크드 리스트로서
다음 노드 포인터
};
host와 service,
소켓 주소의 두개의 구성 요소를 주어주면,
getaddrinfo는
host와 service에 대응되는 소켓 주소 구조체를 가리키는
addrinfo 구조체의 연결리스트를 가리키는 result를 리턴한다.
그래서 addrinfo는 검색 힌트를 제공하는
구조체의 포인터고,
result는 결과를 저장할 구조체의 포인터다.
클라이언트가 getaddrinfo 호출 하여,
이 리스트를 전체 방문하면서,
할때까지 진행한다.
서버에서는 socket과 bind가 성공,
식별자가 유효한 소켓 주소에 연결 될때까지 반복한다.
단, 메모리 누수를 피하기 위해
최종적으로는 freeaddrinfo를 사용해서
반환해야한다.
만약 getaddrinfo가 에러 코드를 반환하면,
응용 프로그램은 gai_strerror을 호출하여
이 코드를 메시지 문자로 변환한다.
getaddrinfo의 host인자는
도메인 이름일수도, 숫자 주소일수도 있다.(그 점과 십진수짜리.)
service 인자는 서비스 이름이거나
십진수 포트 번호일 수 있다.
만약 호스트 이름을 주소로 변환하고 싶지않다면
host를 NULL로 설정이 가능하며 서버도 가능하지만
둘다 NULL로 하는 것은 불가하다.
hints 인자는 선택적으로 사용할수 있고,
구조체 포인터지만,
hints 인자로 전달시에는
ai_family, ai_socktype, ai_protocol, ai_flags
필드만 설정할수 있다.
다른 필드는 NULL이나 0으로 설정되어야한다.
그래서 memset으로 초기화하고 선택 필드만 설정한다.
...
getaddr이
addrinfo 구조체를 생성하면,
ai_flags를 제외한 모든 필드를 채워준다.
ai_addr 필드는 소켓 주소 구조체를 가리키고,
ai_addrlen 은 소켓 주소 구조체 크기를 알려주며,
ai_next_field는 이 리스트에서 다음 addrinfo 구조체를 가리킨다.
이 함수만 있으면,
추가적 조작없이
소켓 인터페이스에 그대로 전달할수 있다.
예를 들어,
ai_family, ai_socktype, ai_protocol은
connect와 bind로 직접 전달될 수 있다.
이러한 특성이 우리가 IP 프로토콜의
특정 버전에 의존하지 않으면서 클라이언트와 서버 작성을 도와준다.
getnameinfo 함수는
getaddrinfo의 역이다.
소켓 주소 구조체를 대응되는 호스트와 서비스 이름 문장으로 변환한다.
gethostbyaddr과 getservbyport의 현대적 버전이다.
이 함수는 재진입 가능하고,
프로토콜 독립적이다.
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa,
socklen_t salen,
char *host,
size_t hostlen,
char *service,
size_t servlen,
int flags);
//성공시 0, 실패시 에러코드 반환.
sa는 salen 바이트의 소켓 주소 구조체를 가리키고,
host는 hostlen 바이트 길이의 버퍼를,
service는 servlen 바이트의 버퍼를 가리킨다.
getnameifo 함수는
소켓 주소 구조체 sa에 대응되는
호스트와 서비스 이름으로 변환하고,
이를 hostdhk service 버퍼로 복사한다.
만약 getnemeinfo가 0이 아닌 에러코드를 리턴하면,
gai_strerror를 호출해 스트링으로 변환할 수 있다.
만약 호스트 이름을 원치 않는다면
host를 NULL로, hostlen을 0으로 설정할 수 있고,
서비스에도 같은 방식이 가능하나,
적어도 하나는 설정되어야한다.
flags 인자는
비트마스크로 기본 동작을 수정한다.
여러가지 값들을 OR해서 생성한다.
아래는 HOSTINFO로,
getaddrinfo와 getnameinfo를 이용해
도메인 이름과 연관된 IP 주소로의 매핑을 출력한다.
(11.3.2의 NSLOOKUP과 유사하다.)
#include "csapp.h"
int main(inc argc, char **argv)
{
struct addrinfo *p,*listp, hints;
char buf[MAXLINE];
int rc, flags;
// 명령행 인자가 2개가 아니라면 종료한다.
// 커널에서 인자를 꼭 2개 반환하게 하도록 함.
if (argc != 2) {
fprintf(stderr, "usage: %s <domain name>\n",argv[0]);
exit(0);
}
//첫번째 인자의 addrinfo를 얻어온다. 실패시 에러처리.
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0){
fprintf(stderr, "getadrrinfo error: %s\n", gai_strerror(rc));
exit(1);
}
//만든 리스트 구조체를 순회. 구조체 안 모든 주소를 프린트.
flags = NI_NUMERICHOST;
for(p = listp; p; p = p->ai_next){
Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
printf(%s\n", buf);
// 메모리 누수 방지용.
Freeaddrinfo(listp);
exit(0);
}
여기서 service는 NULL로 하여 도메인 이름만 변환하게 된다.
(service를 설정하면, 내가 쓰려는 서비스(HTTP등)에
해당하는 포트 번호까지 결부하여 그 두개 다 충족하는 주소를 반환한다.)
getaddrinfo 호출후, getnameinfo로 해서
IP 주소 등이 출력되게 한다.
#include "csapp.h"
int open_clientfd(char *hostname, char *port);
성공시 fd 반환, 실패시 -1
클라이언트는 위 함수를 호출하여
서버와의 연결을 설정한다.
open_clientfd 함수는
hostname에서 돌아가고,
포트번호 port에 연결 요청을 듣는 서버와 연결을 설정한다.
Unix I/O 함수를 이용하여
입력과 출력에 대해 준비된, 열린 소켓 식별자를 리턴한다.
(연결을 설정하면 열린 거니까,
클라이언트 입장에서 연결된 소켓 식별자가 되겠지.)
int open_clientfd(char *hostname, char *port){
int clientfd;
struct addrinfo hints,*listp, *p;
// IPv4, TCP, 서비스 인자가 포트 번호인 것만.
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_NUMERICSERV;
hints.ai_flags |= AI_ADDRCONFIG;
//힌트 + 받은 인자로 구조체 생성.
Getaddrinfo(hostname, port, &hints, &listp);
//socket, connect 될때까지 리스트 전체 순회.
for(p = listp; p; p = p->ai_next){
// 본래 구조체 인자를 그대로 넣어서 fd 생성.
if((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0 )
continue;
//본래 구조체 인자 그대로 넣어서 conncet 해버린다.
if(connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break;
// connect 안되면 닫고 다른걸로 시도해봐야겠다.
Close(clientfd);
}
// 메모리 누수 방지용 청소.
Freeaddrinfo(listp);
//모든 연결 실패.
if (!p)
return -1
//커넥트 성공했었더라...
else
return clientfd;
}
연결 요청을 받을 준비가 된,
듣기 식별자를 생성한다.
#include "csapp.h"
int open_listenfd(char *port);
fd 반환시 성공, 아니면 -1로 실패.
int open_listenfd(char *port)
{
struct addrinfo hints, *listp, *p;
int listenfd, optval=1;
// 힌트 TCP, 듣기 소켓용 주소 리턴, IPv4로 설정,
// service 인자를 포트 번호로 받기.
memset(&hints, 0, sizeof(struct addrinfo);
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
hints.ai_flags |= AI_NUMERICSERV;
// 설정한 것으로 구조체 만들기.
Getaddrinfo(NULL, port, &hints, &listp);
// 구조체를 전체 순회하면서...
for(p = listp; p; p- p->ai_next){
//socket 함수로 fd 만들고...
if((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol))<0)
continue;
// 뭔가 해버린다음..
// 소켓 옵션 설정 함수. SO_REUSEADDR을 쓰고 있으며,
// 소켓이 종료된 후에도 빠르게 같은 포트를 사용할수 있도록 하는 옵션.
Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int));
// 받은 fd로 bind 한다...
if(bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break;
Close(listenfd);
|
Freeaddrinfo(listp);
if(!p)
return -1
// 그다음 받은 listenfd로 listen 시켜놓음! 뒤엣건 Que 용량.
if(listen(listenfd, LISTENQ) <0){
Close(listenfd);
return -1;
}
return listenfd;
}
Setsockopt에서 특히 SO_REUSEADDR은
서버가 종료 되고 재 시작되는 간격이 짧은 경우
본래 이전 포트 및 패킷을 전부 소멸한 뒤에야
TIME_WAIT가 풀리는데,
서버 종료 재시작 후에도 이 상태가 지나야하지만,
이 옵션을 쓰면 서버 종료, 재시작 후 바로 포트를 돌리기에 용이하다.
(책에서는
재시작된 서버는 기본적으로
클라이언트 요청을 거부하기때문에,
이 점에서 디버깅이 어려워지므로
넣기를 추천하고 있다.)
echo 클라이언트의 무한 루틴.
#includ "csapp.h"
int main(int argc, char **argv)
{
int clientfd;
char *host, *port, buf[MAXLINE];
rio_t rio;
//인자 3개 주세요
if(argc != 3){
fprintf(stderr, "usage: %s
<host> <port>\n", argv[0]);
exit(0);
}
// host가 두번째 인자, port가 세번째 인자.
host = argv[1];
port = argv[2];
//fd 받아오고 읽을 준비 중....
clientfd = Open_clientfd(host, port);
Rio_readinitb(&rio, clientfd);
//아무것도 안 줄때까지 받은거 써! 그리고 받은거 읽어!
//그리고 읽는대로 다 콘솔에 써줘!
while(Fgets(buf, MAXLINE, stdin) != NULL){
Rio_wirten(clientfd, buf, strlen(buf));
Rio_readlineb(&rio, buf, MAXLINE);
Fputs(buf, stdout);
}
// 다 읽었다 돌아갈게
Close(clientfd);
exit(0);
|
}
echo 서버의 무한 루틴.
#include "csapp.h"
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};
//인자 2개만 받을게
if(argc != 2){
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
// 듣기소켓으로 쓸 주소 가져왔다
listenfd = Open_listenfd(argv[1]);
//듣기 식별자로 Accept로 연결 식별자 받아와서
//현재 누구랑 연결되었는지 확인하구
//연결 식별자를 echo 함수에 넘겨야징
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);
}
while 안은
연결 요청을 기다리고,
연결된 클라이언트 포트를 출력,
echo 함수를 호출한 후에는 연결 식별자를 닫아준다.
여기서
struct sockaddr_storage clientaddr;
은 소켓 주소 구조체인데
accept에 의해
연결의 다른 쪽 끝의 클라이언트 소켓 주소로 채워진다.
단, clientaddr은 sockaddr_in이 아닌
sockaddr_storage 형으로 선언되었다.
이 구조체는 모든 형태 소켓 주소를 저장하기에 충분히 커서,
코드를 프로토콜 독립적으로 유지하게 해준다.
현재 우리의 서버는 한번에 한개의 클라이언트만 처리하고 있고,
이런 식의 서버를 반복 서버(iterative server)라고 한다.
아래는 echo 함수다.
#include "csapp.h"
void echo(int connfd)
{
size_t n;
char buf[MAXLINE]:
rio_t rio;
//read 준비
Rio_readinitb(&rio, connfd);
//데이터가 있는동안 루프 돌리기
//(받은 데이터 바이트 출력 후 Writen.
while((n= Rid_readlineb{&rio, buf, MAXLINE)) != 0){
printf("server received %d bytes\n", (int)n);
Rio_writen(connfd,buf,n);
}
}