커널에 내부 버퍼를 가지고 있고, 꽉 차면 더이상 전송과 수신을 하지 않으므로 데이터 손실이 없다.
리눅스는 소켓 조작을 파일 조작과 동일하게 간주한다. 즉, 소켓을 파일의 일종으로 구분한다.
네트워크의 연결에는 TCP와 UDP가 나뉘지만, 해당 포스팅에서는 TCP를 다룬다.
함수
서버 측에서의 소켓과 클라이언트 측에서의 소켓은 사용법이 다르다.
서버 측
#include<sys/socket.h>intsocket(int domain, int type, int protocol); // 소켓 생성intbind(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 소켓에 주소 정보 할당intlisten(int sockfd, int backlog); // 소켓을 연결요청이 가능한 상태로 전환intaccept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 연결 요청에 대한 수락
socket() 을 호출하면 소켓을 관리할 수 있는 파일 디스크립터 (이하 fd)를 반환한다.
intmakeSocketAndListen(int portNumber){
structsockaddr_inserver_addr;int socketFd;
if ((socketFd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
return-1;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // IPv4 프로토콜 주소 체계
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 컴퓨터의 IP주소가 자동으로 할당
server_addr.sin_port = htons(portNumber);
if (bind(socketFd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
return-1;
if (listen(socketFd, LISTEN_QUEUE_SIZE) == -1)
return-1;
return socketFd;
}
함수 인자 설명
소켓 프로토콜 체계
PF_INET// IPv4 인터넷 프로토콜 체계PF_INET6// IPv6 인터넷 프로토콜 체계PF_LOCAL// 로컬 통신을 위한 UNIX 프로토콜 체계PF_PACKET// Low level 소켓을 위한 프로토콜 체계PF_IPX// IPX 노벨 프로토콜 체계
: socket() 의 첫 번째 인자를 통해서 지정한 프로토콜 체계의 범위 내에서 프로토콜을 결정해야 한다.
소켓의 타입
: socket()의 두 번째 인자를 통해서 소켓의 데이터 전송방식을 정해준다.
SOCK_STREAM // 연결지향형
SOCK_DGRAM // 비 연결지향형
1. 연결지향형 소켓 (SOCK_STREAM) - 중간에 데이터가 소멸되지 않고 목적지로 전송된다. - 전송 순서대로 데이터가 수신된다. - 전송되는 데이터의 경계가 존재하지 않는다. - 다른 연결지향형 소켓 하나와만 연결이 가능하다.
2. 비 연결지향형 소켓 (SOCK_DGRAM) - 전송된 순서에 상관없이 가장 빠른 전송을 지향한다. - 전송된 데이터는 손실과 파손의 우려가 있다. - 전송되는 데이터의 경계가 존재한다. (두 번을 보내면 두 번을 받아야 한다.) - 한 번에 전송할 수 있는 데이터의 크기가 제한된다.
프로토콜 정보 추가 전달
:socket()의 세 번째 인자를 통해서 프로토콜의 정보를 추가 전달해준다.
IPPROTO_TCP // IPv4, 연결지향형
IPPROTO_UDP // IPv4, 비 연결지향형
sockaddr의 sa_data에 저장되는 주소정보는 ip주소와 port 번호가 포함되어야 하고, 남은 부분은 0으로 채울 것을 bind()가 요구하는데 이는 매우 불편해서 sockaddr_in 구조체가 나왔다.
sockaddr_in의 바이트 열이 bind()가 요구하는 바이트 열이 되어 형변환해서 전달 시 sockaddr 구조체 변수에 bind()가 요구하는 대로 데이터를 채워 넣은 효과를 볼 수 있다.
1. sin_port: 16비트 port 번호를 저장, 네트워크 바이트 순서 2. sin_addr: 32비트 IP주소정보를 저장, 네트워크 바이트 순서 3. sin_zero: sockaddr_in의 크기를 sockaddr과 일치시키기 위한 멤버, 반드시 0으로 채워야 한다.
Port
IP만 있다면 목적지 컴퓨터로 데이터를 전송할 순 있지만 최종 목적지인 응용프로그램에까지 데이터를 전송할 순 없다.
운영체제는 PORT 번호를 활용해 해당 소켓에 데이터를 전달한다.
따라서, 하나의 운영체제 내에서 동일한 PORT 번호는 하나의 소켓에만 할당 가능하다.
- PORT는 16비트로 표현하기 때문에 0 ~ 65535 까지다. - 0 ~ 1023은 예약된 포트라 사용할 수 없다. - TCP PORT와 UDP PORT는 중복되어도 상관없다.
네트워크 바이트 순서
1. 빅 엔디안 - 상위 바이트의 값을 작은 번지수에 저장 (1 저장시, 0000 0000 0000 0001) - 네트워크상으로 데이터를 전송할 때 사용함
2. 리틀 엔디안 - 상위 바이트의 값을 큰 번지수에 저장 (1 저장시, 0001 0000 0000 0000) - 인텔계열 CPU에서 사용함
3. 바이트 순서의 변환 - short 버전은 port 번호의 변환에 사용함 - long 버전은 ip 주소의 변환에 사용함
unsignedshorthtons(unsignedshort); // host to network (short) unsignedshortntohs(unsignedshort); // network to host (short) unsignedlonghtonl(unsignedlong); // host to network (long) unsignedlongntohl(unsignedlong); // network to host (long)
4. sockaddr 구조체 변수에 데이터를 채울 때 이외에는 신경쓰지 않아도 된다. 변환이 자동으로 이루어지기 때문이다.
연결 대기 상태로 전환한다. 서버 소켓은 대기 큐를 관리하는 문지기이고, 데이터를 주고받으려면 소켓이 따로 필요하지만 직접 만들 필요는 없다. accept()가 성공 시 생성된 소켓의 파일 디스크립터를 반환하기 때문이다.
두번째 인자 backlog : 연결요청 대기 큐 크기 (실험적 결과에 의존, 웹의 경우 최소 15)
connect()
서버 소켓의 대기 큐에 등록한다.
첫번째 인자 servaddr : 연결 요청할 서버의 주소 정보
두번째 인자addrlen : 두 번째 매개변수에 전달된 주소의 변수 크기 (바이트)
세번째 인자sock : 클라이언트 소켓의 파일 디스크립터
소켓 프로그래밍 시 주의점
1. 서버는계속 켜져있으며 연결요청을 하는 모든 클라이언트에게 서비스를 제공해야 한다. * 소켓을 close()하면 상대방 소켓에게 EOF가 전달됨 (예로 상대 read의 반환값이 0)
2. 서버는 한 번의 write 호출로 데이터를 전송했지만, 전송할 데이터의 크기가 크다면 운영체제가 내부적으로 이를 여러 번으로 나눠 전송할 수도 있다. 이 과정에서 모두 전송되지 않았음에도 불구하고 클라이언트는 read함수를 한 번 호출하여 원하는 결과를 얻지 못하게 된다.
- echo 서버의 경우는 클라이언트에게 받은 문자열 길이 만큼 다시 보내야하기 때문에
보낼 문자열의 길이를 기억하고 있다가 write로 반환된 문자열의 길이를 누적하여 같아질 때까지 반복하여 해결 - 에코가 아닌 경우는 그들만의 프로토콜로 정의함
* write()는 전송할 데이터가 내부 출력버퍼로 이동이 완료되는 시점에 반환된다.
TCP의 경우 전송을 보장하기 때문에 데이터가 전송이 완료되어야 반환된다.
3. TCP의 Half-close : 한 쪽에서 close()를 하면 연결이 종료되기 때문에 꼭 받아야 할 데이터도 전송받지 못한다.
소켓을 생성하면 A의 출력 버퍼에서 B의 입력 버퍼로 흘러가는 흐름이 형성되고,
B의 출력 버퍼에서 A의 입력 버퍼로 흘러가는 스트림이 형성된다. 이 중 하나만 끊는 게 Half-close이나 close()는 두 개를 모두 끊어버린다.
이 문제를 해결하기 위해 스트림의 일부만 종료하는 방법이 있다.
#include<sys/socket.h>intshutdown(int sock, int howto);
- howto : 종료 방법에 대한 정보 전달
SHUT_RD// 입력 스트림 종료SHUT_WR// 출력 스트림 종료SHUT_RDWR// 입출력 스트림 종료
출력 스트림을 종료하면 상대 호스트로 EOF 전달, 따라서 서버는 Half-close로 출력 스트림을 닫는다.
send(), recv()
#include<sys/socket.h>ssize_tsend(int sockfd, constvoid* buf, size_t nbytes, int flags);
ssize_trecv(int sockfd, void* buf, size_t nbytes, int flags);
1. sockfd : 데이터 수신 대상과의 연결을 의미하는 소켓의 fd
2. buf : 수신된 데이터를 저장할 버퍼의 주소 값
3. nbytes : 수신할 수 있는 최대 바이트 수
4. flags : 데이터 수신 시 적용할 다양한 옵션 정보 (비트 OR 연산 사용 가능)
Flag
Description
MSG_OOB
긴급 데이터의 전송을 위함 (out-of-band) TCP 헤더에 긴급 데이터 존재 여부와 offset을 저장하여 1바이트만 알려줌하지만 그렇게 긴급으로 전송되진 않음, 전혀 다른 통신 경로로 전송되는 데이터를 뜻함
MSG_PEEK
입력 버퍼에 수신된 데이터의 존재 유무를 확인입력 버퍼에 존재하는 데이터가 읽혀지더라도 입력버퍼에서 데이터가 지워지지 않음. 데이터가 존재하지 않으면 blocking 됨
MSG_DONTROUTE
데이터 전송과정에서 라우틴 테이블을 참조하지 않고 로컬 네트워크상에서 목적지를 찾음
MSG_DONTWAIT
입출력 함수 호출과정에서 blocking 되지 않을 것을 요구함
MSG_WAITALL
요청한 바이트 수에 해당하는 데이터가 전부 수신될 때까지 호출된 함수가 반환되는 것을 막음
read(), write()와 대체로 비슷하지만 마지막 인자인 flags의 유무가 다르다.
socket을 통한 I/O를 할 때는 send(), recv()가 더 가볍다.
fcntl()
#include<fcntl.h>intfcntl(int fd, int cmd, ...); // 성공 시 매개변수 cmd에 따른 값
: 소켓을 Non Blocking 특성으로 변경한다.
// 두 번째 인자F_GETFL// fd에 설정되어 있는 특성정보를 int형으로 얻을 수 있음F_SETFL// fd의 특성정보를 변경
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
: 기존에 설정되어 있던 특성정보를 얻어오고, Non Blocking 입출력을 의미하는 특성을 더해준다.
read & write 함수 호출시에도 데이터의 유무에 상관없이 블로킹이 되지 않는 소켓이 만들어진다.
소켓을 Non Blocking으로 변경해줌으로써 데이터의 수신 시점과 데이터가 처리되는 시점을 분리할 수 있다.
I/O Blocking, Non-Blocking 및 I/O 멀티플렉싱에 대한 더 자세한 설명한 아래 링크 참고