API
API란 시스템이 어플리케이션에 제공하는 인터페이스이다.
API에는 여러 종류가 있는데 우리는 TCP/IP에서 쓰이는 다양한 API중 Sokcet에 대해 알아 볼 예정이다.
TCP/IP에 쓰이는 API는 Socket말고도 TLK, XTI, Winsock, MacTCP등 여러 종류가 있다.
Sokcet
소켓은 다섯개의 componet와 관련이 있다.
또한 아래의 함수들은 <sys/types.h> 헤더파일과 <sys/socket.h> 헤더파일에 존재한다.
1. Protocol
- 어떤 프토토콜을 사용할 건지
- socket()함수의 argument로 어떤 프로토콜을 사용할 건지 알려준다.
2. Source's address and port number
- 보내는 곳의 주소와 포트넘버
- socket()함수는 socket descriptor(OS에서의 file descriptor와 같다)를 반환하는데, 이 socket descriptor와 source의 주
소, 포트넘버를 bind()함수로 연관시킨다.
3. Destination's address and port number
- 받는 곳의 주소와 포트넘버
- TCP(연결지향)에서는 connect()함수로 socket descriptor와 des의 주소, 포트넘버를 묶는다.
- UDP(비연결지향)에서는 sendto()함수의 argument로 목적지의 주소와 포트넘버만 넣어서 보낸다.
Creating a socket
다음 함수를 통해 소켓을 생성할 수 있다.
int socket(int family, int type, int protocol)
family : 어떤 프로토콜 family를 사용할 건지
type : 해당 family중에서 어떤 type을 사용할 건지
- SOCK_STREAM : TCP // SOCK_DGRAM : UDP
protocol : 해당 type중에서 어떤 protocol을 사용할 건지
socket()은 system call로, 반환값은 socket descriptor이다. 이때 소켓생성에 실패하면 -1을 반환한다.
다음과 같이 생성할 수 있다.
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
int sockfd;
if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) { // IPv4에서의 TCP사용
/* print “socket error + the error message */
perror(“socket error”); exit(1);
}
Binding the local address
bind()함수는 다음과 같이 사용할 수 있다.
소켓에 주소를 할당해주기 위한 시스템 콜이다.
보통 server쪽에서 하고 client에선 하지 않는다. (client에선 무작위로 port number 할당)
int bind(int sockfd, struct sockaddr *addr, int addr_len)
int sockfd : 생성 소켓(socket())에서 반환된 socket descriptor
struct sockaddr *addr : socket의 구조체 포인터 (자신의 address와 port number가 들어있음)
int addr_len : 구조체의 길이
성공시 0을 리턴하고 실패시 -1을 리턴한다.
소켓 구조체는 다음과 같이 이루어져 있다.
#define <netinet/in.h>
struct sockaddr {
u_char sa_len; /* length : used in kernel */
u_short sa_family; /* address family */
char sa_data[14]; /* address */
}
위의 sockadrr를 인터넷에서 쓰기위한, 그리고 sa_dat[14]를 조금 더 세분화해서 나눈 sockaddr_in도 있다.
// <netinet/in.h> 헤더파일에 정의되어 있다.
struct sockaddr_in {
u_char sin_len; /* length */
u_short sin_family; /* AF_INET */
u_short sin_port; /* port number */ //2byte
struct in_addr sin_addr; /* IP addess */ //4byte
char sin_zero[8]; /* unused */ //8byte
}
struct in_addr {
u_long s_addr; /* 32 bit IP address */
}
앞선 sa_data 14byte짜리를 sin_port(2byte) + sin_addr(4byte) + sin_zero(8byte)로 나눈 모습이다.
위 두 종류의 차이를 비교해보면 다음과 같다.
왼쪽이 sockaddr
오른쪽이 sockaddr_in
사실 두개는 같지만 오른쪽에서 sa_data를 나누어 필요한 6byte에만 포트넘버와 주소를 넣고 나머지는 0으로 채워넣는 것을 볼 수 있다.
다음과 같이 사용할 수 있다.
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MYPORT 50000 // 나의 포트넘버
int sockfd;
struct sockaddr_in my_addr; //socket 구조체 선언
if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) { //소켓 생성
perror(“socket error”);
exit (1);
}
memset(&my_addr, 0, sizeof(my_addr)); //socket 구조체 초기화
my_addr.sin_family = AF_INET; //address family
//PF_INET과도 같지만 정확히는 AF_INET
my_addr.sin_port = htons(MYPORT); /*나의 포트넘버를 htons()를 통해 변환
htons는 host에서 사용하는 number를 network에서 사용가능하도록
short하게 변환함*/
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* INADDR_ANY는 현재 나의 주소 반환
htonl은 host에서 사용하는 주소를 network에서 사용가능하도록
long하게 변환함*/
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
perror(“bind error” );
exit(1);
}
코드에서 htons와 htonl이 있는 이유는 Byte odering(big endian, little endian)을 위해 존재한다. (네트워크상과 호스트상의 endian방식이 다름)
Name-to-Address Conversion
앞선 예제에서 INADDR_ANY와 같이 주소를 바로넘겨줄 수 도 있지만, 이름만 알고 있을 때 그 이름을 주소로 바꾸는 과정이 필요하다. 또한 반대로 주소를 알고 있을 때, 호스트의 이름을 얻을 수도 있다.
이때 필요한 함수가 gethostbyname()과 gethostbyaddr()이다.
이 두 함수는 <netdb.h>헤더파일에 포함되어 있다.
호스트 네임을 통해 주소를 알기 위해선 다음함수가 필요하다.
struct hostent *gethostbyname(const char *name);
호스트 주솔를 통해 네임을 알기 위해선 다음함수가 필요하다.
struct hostent *gethostbyaddr(const char *addr, int length, int type);
위 두함수를 자세히 보면 hostent라는 구조체의 포인터를 반환하는데 이 구조체는 다음과 같이 구성되어 있다.
#define h_addr haddr_list[0]
struct hostent {
char *h_name; /* host name */
char **h_alias; /* list of alternate names */
int h_addrtype; /* type of address = 2 (=AF_INET) */
int h_length; /* address length = 4 for IPv4 */
char **h_addr_list;; /* address list */
};
위 두 함수에 대한 예제는 다음과 같다.
/*gethostbyname*/
struct hostent *phost;
struct in_addr **addr_list;
if ((phost = gethostbyname(“www.handong.edu”)) == NULL) {
perror("gethostbyname");
return 1;
}
// print information about this host:
printf("Official name is: %s\n", phost->h_name);
printf(" IP addresses: ");
addr_list = (struct in_addr **)phost->h_addr_list;
for(i = 0; addr_list[i] != NULL; i++) {
printf("%s ", inet_ntoa(*addr_list[i]));
}
printf("\n");
/*gethostbyaddr*/
struct hostent *phost;
struct in_addr addr;
inet_aton(“203.252.97.12", &addr);
phost = gethostbyaddr(&addr, sizeof(addr), AF_INET);
printf("Host name: %s\n", phost->h_name);
IP Address Manupulation
위 함수를 통해 네임을 binary한 주소로 바꿨다면 우리가 알아볼 수 있게 printable하게도 변환할 수 있어야 좋을 것이다. (e.g. 255.0.0.1) 이를 Dotted decimal이라고 한다.
IP 주소를 dotted decimal로 바꿔주는 함수
char *inet_ntoa(struct in_addr address);
inet(인터넷의) ntoa(network에서 아스키로) 즉, binary한 주소를 dotted decimal로 바꿔준다.
또한 반대로 dotted decimal을 IP 주소로 바꿀 수도있다.
두 가지 방법이 있다.
int inet_aton(const char *cp, struct in_addr *inp); //return 0 if error
*cp에 dotted decimal을 넣으면 *inp 구조체 안에 변환된 주소를 넣어준다.
parameter로 넘겨준 구조체 안에 넣어주는 방식이 아닌 바로 IP주소를 리턴해주는 방식도 있다.
u_long inet_addr(char *dottedAddress); //return -1 if error
이때 주의할 점은 반환형이 unsigned_long이기 때문에 -1를 리턴하면 0xfffff... 로 리턴이 될텐데 이를 dotted decimal로 표현하면 255.255.255.255이다.
그러나 실제로 255.255.255.255를 쓰는 곳이 있기 때문에 error가 발생했는지 실제 주소가 255.255.255.255인지 판별할 수가 없을 경우가 생긴다.
이럴때는 위의 inet_aton()을 사용한다.
예제는 다음과 같다.
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet.h/in.h>
#include <arpa.h>
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
inet_aton(“10.12.110.57”, &(my_addr.sin_addr));
/* my_addr.sin_addr.s_addr = inet_addr(“10.12.110.57”); */
printf(“%s”, inet_ntoa(my_addr.sin_addr);
위 코드처럼 inet_aton()함수를 써도 되고 주석된 line처럼 inet_addr를 통해 반환해도 된다.
지금까지의 변환 과정을 그림으로 보면 다음과 같다.
Connect
서버가 bind를 한 뒤에 서버는 connect를 해야한다. (서버에 연결요청)
성공시 0을, 실패시 -1을 리턴한다.
3-handshaking 과정을 통한 커넥션을 요청하는 것이기 때문에 TCP에 사용된다.
근데 아주가끔 UDP에도 사용된다고 한다.
- sendto()함수를 쓸 때 목적지의 정보를 넣어야 하는데 보낼때마다 가져오기 귀찮으니까 connect를 한번 하고(실제 데이터 전송은 없음) 그때 얻은 정보를 계속 사용
int connect(int sd, struct sockaddr *addr, int addr_len);
이때 addr은 서버의 address이다.
사용 예제는 다음과 같다.
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define DEST_IP “10.12.110.57”
#define DEST_PORT 23
int sockfd;
struct sockaddr_in dest_addr;
if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
/* error */
}
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(DEST_PORT);
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
if (connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) !=0 ) {
close(sockfd);
return -1;
}
Listen
서버가 bind를 한 뒤에 client로부터 request를 기다리는 과정이다.
성공시 0을, 실패시 -1을 리턴한다.
connection request를 기다리는 함수기 때문에 TCP에만 사용된다.
int listen(int sd, int backlog);
sd는 바인딩할때 사용했던 socket descriptor이다.
backlog는 connection을 할때 queue에 몇개까지의 request를 pending상태로 놓을 것인지에 대한 사이즈인데 대개 5로 놓고 사용하고, 구현에 따라 맞지않는 경우도 많다고 한다.
Accpet
listen상태에 있는 서버가 client로부터 connection을 받아들이는 함수이다.
이 역시 TCP에서만 사용한다.
성공시 새로운 socket descriptor를 반환하고, 실패시 -1을 반환하다.
이때 왜 새로운 socket descriptor를 반환하냐?
TCP는 1:1통신을 지원하는데 한 서버가 클라이언트1과 통신한 후에 클라이언트2와 새로 통신하는 것을 구별하기 위함이다.
int accept(int sd, struct sockaddr *addr, int *addrlen);
addr은 client의 주소정보이다.
지금까지 알아보았던 과정(상태)을 그림으로 나타내면 다음과 같다.
SYN이 왔다는 것은 client가 요청을 connection요청을 보냈다는 것이다.
지금까지 나온 과정에 대한 서버에서의 예제코드는 다음과 같다.
잘 따라가보며 이해해보자
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MYPORT 50000
int main()
{
int sockfd, new_fd;
struct sockaddr_in my_addr, client_addr;
int sin_size;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
listen(sockfd, 5);
sin_size = sizeof(client_addr);
new_fd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size);
return 0;
}
지금까지의 과정은 데이터를 전송하기 전 연결을 완료하기 위한 과정이었다.
TCP와 UDP의 각 필요했던 함수들을 정리해보면 다음과 같다.
TCP | UDP | ||
Server | Client | Server | Client |
socket() | socket() | socket() | socket() |
bind() | connect() | bind() | |
listen() | |||
accept() |
여기까지 서버와 클라이언트가 서로 연결되었다.
Sending and Receiving data (TCP)
연결을 마쳤으니 이제 데이터를 실제로 쓰고 읽어야 한다.
read()
int read(int fd, char *buf, int buflen)
성공시 읽은 바이트 수를 리턴하고 실패시 -1을 리턴한다.
만약 0리턴 시 상대방 측에서 커넥션을 close한 상태이다.
또한 block성질을 가지기 때문에 데이터가 올때까지 기다리고 있는다.
write()
int write(int fd, char *buf, int buflen)
성공시 쓴 바이트 수를 리턴하고 실패시 -1을 리턴한다.
send()
int send(int sockfd, const void *msg, int len, int flag)
write()와 똑같다.
성공시 쓴 바이트 수를 리턴하고 실패시 -1을 리턴한다.
flag는 보통 0으로 쓴다. 다른걸 쓰는건 못봤다고 하신다.
예제를 들자면 다음과 같다.
char *msg = “Hi, Beej!”;
int len, bytes_sent;
len = strlen(msg);
byte_sent = send(sockfd, msg, len, 0);
recv()
int recv(int sockfd, void *buf, int len, unsigned int flag)
read()와 똑같다.
성공시 읽은 바이트 수를 리턴하고 실패시 -1을 리턴한다.
또한 역시 block성질을 가지고 있고 만약 0리턴시 상대방 측에서 커넥션을 close한 것이다.
flag는 역시 0을 쓴다.
자, 그러면 read()/write()와 send()/recv()의 차이는 무엇인가?
없다. 그냥 자기 OS에서 지원해주는 함수 아무거나 쓰면 된다.
Sending and Receiving data (UDP)
sendto()
int sendto(int sockfd, const void *msg, int len, int flag, struct sockaddr *dstaddr, int addrlen)
UDP에서 사용하는 send함수이다. TCP에서와의 차이점은 목적지의 주소를 넣는 것 뿐이다.
역시 성공시 보낸 byte의 수를 리턴하고 실패시 -1을 리턴한다.
recvfrom()
int recvfrom(int sockfd, void *buf, int len, int flags, struct sockaddr *srcaddr, int *addrlen)
UDP에서 사용하는 recv함수이다.
역시 성공하면 읽은 byte수를 리턴하고 실패시 -1을 리턴한다.
UDP의 특성상 여러 client에서 목적지의 주소를 알고 전송하면 해당 목적지는 다 받아버리기 때문에 데이터가 어디에서 왔는지 source에 대한 주소를 저장하는 부분이 있다.
Close and Shutdown
close()
int close(int sockfd)
TCP에서 클라이언트가 커넥션을 끊고싶을 때 사용한다.
서버는 클라이언트가 close()상태 인지 어떻게 알까?
만약 클라이언트가 close()를 호출한 상태인데 서버가 recv()를 호출하면 0을 리턴한다.
또한 만약 이상태에서 서버가 클라이언트에게 send()를 보내면 -1(error)를 리턴한다. (클라이언트 쪽에서는 close()상태인데 데이터를 받았으므로 에러 시그널 : SIGPIPE, 서버쪽의 send()의 리턴은 에러 시그널 : EPIPE)
shutdown()
int shutdown(int sockfd, int how)
연결을 끊는게 아닌 보내거나 받는기능만 닫는 것이다.
이는 how에서 판단한다.
how에 올 수 있는 경우는 다음과 같다.
이때, 2번(SHUT_RDWR)이 close()와 같다고 생각할 수 있는데 조금 다르다.
close()는 연결 자체를 닫는 것이고 shutdown에서의 SHUT_RDWR은 소켓정보는 유지한 채 보내고 받는 기능만 닫는 것을 의미한다.
오른쪽 그림과 같이 서버가 모든 파일을 전송 후에 끝냈다는 신호를 보내고 싶은데 빨간지점에서 close()를 호출해 버리면 EOF를 보낼 수 없다.
이런 상황에서 shutdown을 사용해 빨간지점에서 SHUT_WR을 통해 보내는 기능만 닫고 EOF를 받은 클라이언트는 SHUT_RD를 이용해 읽는기능을 닫은 후에 서버가 파란지점에서 최종적으로 close()를 한다.
'컴퓨터 네트워크 > 소켓 프로그래밍' 카테고리의 다른 글
TCP 소켓프로그래밍으로 웹서버 구현하기 (0) | 2021.11.17 |
---|---|
Socket Programming (소켓 프로그래밍) (2) (0) | 2021.10.29 |