상세 컨텐츠

본문 제목

UNIX - Sockets TCP, UDP

Computer Science/UNIX

by 2021. 12. 2. 03:25

본문

반응형

Sockets

Socket은 TCP/IP 통신을 위해서 사용된다. 

Internet model은 위와 같은 각 계층으로 이루어져 있다.

 

 

node와 node 사이 통신을 Data link layer,

host와 host 사이 통신을 Network layer,

process간의 통신을 Transport layer라고 한다. 

Transport layer에 TCP와 UDP가 있다.

 

PC의 주소를 지정하는데 IP 주소가 사용된다. 하나의 PC에서도 여러가지 process가 있는데, 이 여러 process 중 특정 process하나를 지정하는데 사용하는 것이 port number이다.

즉 통신을 위해서는 IP주소와 port number가 필요하다. 따라서 socket 통신을 위한 socket address에 IP주소와 port number가 포함된다. 

 

 

socket 또한 file로 다루기 때문에 open하고 file descriptor를 얻어 사용한다. read / write system call 을 통해 사용할 수 있다.

internet을 통하여 원격으로 통신하고자 할 때, socket의 file descriptor를 통해 read / write하면 된다.

 

Two Type of Internet Sockets

Datagram sockets, which use UDP(User Datagram protocol)

  • connectionless protocol
    프로세스간 통신에서 지정된 경로없이 찾아서 이동
  • flow나 error control이 없어 불안전
  • small message
  • Multicast and broadcast

Stream Sockets, which use TCP(Transmission Control protocol)

  • connection-oriented protocol 
    두 프로세스간 통신에서 connection의 경로가 완전히 설정되고 통신이 이루어짐
  • flow와 error control 메커니즘이 있어 안정적

 

서버 와 클라이언트 소켓 구현의 이해 TCP

소켓 구현과정을 전화기에 비유하면 다음과 같다.

 

Server Socket

먼저 소켓을 생성한다.

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

// Returns: file (socket) descriptor if OK, -1 on error

socket을 통해 file descriptor를 얻는다. 

매개변수 domain에는 network종류가 들어가고, type에는 TCP또는 UDP를 선택하는 상수가 들어간다. protocol은 사용하지 않는다.

 

그 다음 IP주소와 port number를 통해 주소 할당을 요구한다.

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);

// Returns: 0 if OK, -1 on error

sockfd에 socket의 fd가, addr에는 IP주소와 port number, len에는 addr의 길이이다.

 

다음으로 연결 요청 대기 상태로 들어간다.

#include <sys/socket.h>
int listen(int sockfd, int backlog);

// Returns: 0 if OK, -1 on error

 backlog는 queue의 size. 즉 연결 요청 대기 큐 개수이다.

 

연결 요청을 수락한다.

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr,
				socklen_t *restrict len);

// Returns: file (socket) descriptor if OK, 1 on error

addr에 상대방 주소에 대한 IP, port 정보가 들어간다. len는 addr의 길이 값이다.

여기서 반환한 accept의 fd로 실제 read / write를 진행한다. 

 

Client Socket

서버와 같은 방법으로 소켓을 생성 후 연결을 요청한다.

#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *addr,socklen_t len);

// Returns: 0 if OK, -1 on error

addr은 상대방 주소(IP, port) 정보, len은 그 길이이다.

client에서는 주소할당 bind과정이 자동으로 진행된다.

client는 sockfd에 해당하는 fd로 read / write를 진행한다. 

 

 

위 그림과 같이 client - server사이에 socket 통신이 이루어진다.

client - server 간 send / receive가 충분히 이루어지고 나면 client가 close를 통해 연결을 끊는 신호를 보내고, server는 신호를 받은 후 close한다.

server는 여러 개의 client에 대비하여 send / receive 과정을 fork하여 생성한 child에게 할당하고, accept를 parent가 담당한다. 

 

example - Server

#include <stdio.h>  
#include <stdlib.h>	   
#include <string.h>  
#include <unistd.h> 
#include <arpa/inet.h> 
#include <sys/types.h>
#include<sys/socket.h>

int main(int argc, char **argv){
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_addr, clnt_addr;
	int clnt_addr_size;
	char message[]="Hello World!\n";
    
	if(argc!=2){ printf("Usage : %s <port>\n", argv[0]); exit(1); }
    
	serv_sock=socket(PF_INET, SOCK_STREAM, 0); /* 서버 소켓 생성 */	
	if(serv_sock == -1) error_handling("socket() error");
    
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_addr.sin_port=htons(atoi(argv[1]));
    
	if( bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
		error_handling("bind() error"); 
    
	if( listen(serv_sock, 5)==-1 ) error_handling("listen() error");
	clnt_addr_size=sizeof(clnt_addr); 
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size); 
	if(clnt_sock==-1) error_handling("accept() error");  
	write(clnt_sock, message, sizeof(message)); /* 데이터 전송 */  
	close(clnt_sock); /* 연결 종료 */
	return 0;
}

코드에서 sockaddr_in이라는 struct가 나오는데,

sockaddr은 일반적인 주소 표현 struct이고, sockaddr_in은 internet 용도의 address struct이다.

 

PF_INET은 internet, SOCK_STREAM은 TCP를 사용한다는 의미이다. socket으로 반환된 fd는 serv_sock에 저장한다.

serv_addr의 sin_family는 domain, addr과 port는 IP주소와 port number이다.

s_addr의 INADDR_ANY 상수는 자신의 컴퓨터 IP라는 의미이다. 

hton은 바이트 형식 변경으로 나중에 알아보자. 

 

bind로 socket fd와 serv_addr 주소정보, size를 통해 주소할당을 한다. 

 

listen으로 serv_sock의 연결요청 대기 큐 개수를 5로 설정한다.

accept로 clnt_addr에 client 주소를 담고, clnt_addr_size로 최대 size를 설정한다. 

accept에서 return된 fd 즉 clnt_sock 에 담은 fd로 실제 read / write를 진행한다.

 

example - Client

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

int main(int argc, char **argv){
	int sock;
	struct sockaddr_in serv_addr;
	char message[30];
	int str_len;
    
	if(argc!=3){ printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); }

	sock=socket(PF_INET, SOCK_STREAM, 0); 
	if(sock == -1) error_handling("socket() error");
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_addr.sin_port=htons(atoi(argv[2]));

	if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 		
		error_handling("connect() error!");

	str_len=read(sock, message, sizeof(message)-1); /* 데이터 수신 */

	if(str_len==-1) error_handling("read() error!");
	message[str_len]=0;
	printf("Message from server : %s \n", message);  
	close(sock); /* 연결 종료 */
	return 0;
}

마찬가지로 socket에서 PF_INET, SOCK_STREAM으로 internet에서 TCP를 사용하는 것으로 설정한다. 

serv_addr에 domain, IP주소(argv[1]), port number(argv[2])를 설정한다. 

inet_addr이라는 library를 이용하여 IP주소 문자열을 binary 4 byte형태로 변환시킨다. 

port number는 atoi로 문자열을 int로 변환시켜 설정한다. 

 

connect로 server에 연결요청을 시도한다. serv_addr에 server 주소정보, 주소 길이를 넣어 connect를 시도한다.

socket으로 할당한 sock을 이용하여 read / write를 진행한다.

 

서버 와 클라이언트 소켓 구현의 이해 UDP 

UDP는 connection이 되지 않은 상태에서 정보가 전달되는 connectionless protocol이다.

 

server는 여러 client를 상대하며, socket과 bind단계는 TCP와 동일하다. client가 sendto를 할 때는 상대편 주소만 명시하면 되며, recvfrom으로 받을 때는 송신주소와 함께 받는다. connection oriented(TCP)에서는 send / recv를 실행할 때 상대편 주소를 argument로 설정하지 않았지만, UDP에서는 주소 argument가 추가된다.UDP는 connection이 set up되지 않았으므로 close할 필요가 없다. 

 

Addressing

Internet Addressing

인터넷에서는 IP주소와 port number를 이용하여 주소를 명시한다. 

IP주소는 32bit(IPv4)로 165.246.10.2와 같은 형식으로 표현한다.

IP주소에 domain names 을 통해 IP와 매핑시켜 domain을 통해 해당 IP에 접근한다. 

 

#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
// Returns: 1 if string was valid, 0 on error

in_addr_t inet_addr(const char *strptr);
// Returns: 32-bit binary network byte ordered IPv4 address;
			// INADDR_NONE if error

char *inet_ntoa(struct in_addr inaddr);
// Returns: pointer to dotted-decimal string

IP주소는 문자열 형태로 이루어져 있다가 32bit의 이진수로 표현을 바꾸어야 하는데 위 함수를 통해서 변환한다.

inet_aton은 strptr의 문자열을 32bit 숫자로 변환하여 addrptr에 저장한다.

inet_addr은 strptr의 문자열을 32bit 숫자로 return한다.

inet_ntoa는 inaddr 32bit 이진수를 문자열 pointer로 return한다.

 

 

bind에서 사용되는 sockaddr struct는 일반적인 주소형식으로 다양한 네트워크 형식에 맞추어 변형할 수 있도록 설정된 struct이다. 실제로 internet에서 사용할 때는 sockaddr_in struct를 사용한다. 

 

sa_family는 네트워크 종류로 sockaddr에서와 _in에서 동일하고,

sin_port는 port number,

in_addr struct인 sin_addr은 4byte IP주소이다. in_addr struct는 u_long 4byte로 이루어진 IP주소 표현이다. 

이렇게 struct를 사용한 이유는 IP version에 따른 확장성을 위함이다.

sockaddr에서 sa_data에 해당하는 14byte가 sockaddr_in에서는 6byte를 사용하고, sin_zero 8byte는 사용하지 않는다.

 

Byte Ordering

0x12345678

위와 같은 hexa 주소에서 처음 부분(12...)은 high order, 마지막 부분(...78)은 low order 이라고 한다.

 

4byte인 16진수 2개씩 끊어서 전달하는데, 15번지(Big)에 low order를 담아 마지막 순서로 하여 보내는 것을 Big-Endian, 12번지(Little)을 마지막 순서로 하여 보내는 것을 Little-Endian이라고 한다. 

하드웨어마다 Big-Endian인지 Little-Endian인지는 다르므로 이 차이를 극복하기 위해 중간과정에 htons / htonl / ntohs / ntohl 등으로 byte order를 변경하는 과정을 거친다. 

 

Confusing the byte ordering

위 그림과 같이 Big-Endian을 사용하는 하드웨어와 Little-Endian을 사용하는 하드웨어 사이에 정보를 전달하는데 순서상 오류가 생기면 안되므로 중간에 네트워크에서는 network byte order를 따로 사용한다.

 

network byte order는 Big-Endian으로 통일되어있으며, 하드웨어 host에서 데이터를 전송할 때 htons(short 2byte) / htonl(long 4byte)를 사용하여 network byte order로 변환하여 전송하고, 상대변 host가 수신할 때 ntohs(2byte) / ntohl(4byte)를 이용하여 host byte order로 변경하여 데이터를 받는다.

 

The byte order functions

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
// Returns: 32-bit integer in network byte order
 
uint16_t htons(uint16_t hostint16);
// Returns: 16-bit integer in network byte order
 
uint32_t ntohl(uint32_t netint32);
// Returns: 32-bit integer in host byte order
 
uint16_t ntohs(uint16_t netint16);
// Returns: 16-bit integer in host byte order

htonl은 host 주소를 network byte order 32bit 로 변환하여 return한다. 

htons는 host 주소를 network byte order 16bit 로 변환하여 return한다. 

ntohl은 network byte 주소를 host byte order 32bit 로 변환하여 return한다. 

ntohs는 network byte 주소를 host byte order 16bit 로 변환하여 return한다. 

 

Socket interface

The socket(2) system call

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

// Returns: file (socket) descriptor if OK, -1 on error

각 매개변수에 들어가는 값들의 종류는 다음과 같다.

반응형

관련글 더보기