서버소켓과 클라이언트 소켓의 흐름은 다음과 같다.
1. 소켓 생성: Create
2. 서버가 사용할 주소 지정하여 결합(IP, Port): Bind
3. 연결 요청 대기: Listen
4. 요청 수신시 받기: Accept
5. 연결 수립시 데이터 송수신: send/recv
6. 송수신 완료, 소켓 닫기: Close
1. 소켓 생성: Create
2. 서버 연결 요청: Connect
3. 연결 요청 받으면 데이터 송수신: send/recv
4. 송수신 완료, 소켓 닫기: Close
이미지 출처: https://saynot.tistory.com/entry/%EC%86%8C%EC%BC%93-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-TCP-Echo-Client-Server-%EA%B5%AC%ED%98%84
-Socket 함수는 어떤 프로토콜을 가진 소켓으로 통신할 것인지 결정하여 생성하는 함수이다.
PF_INET, AF_INET IPv4 인터넷 프로토콜을 사용합니다.
PF_INET6 IPv6 인터넷 프로토콜을 사용합니다.
PF_LOCAL, AF_UNIX 같은 시스템 내에서 프로세스 끼리 통신합니다.
PF_PACKET Low level socket 을 인터페이스를 이용합니다.
PF_IPX IPX 노벨 프로토콜을 사용합니다.
|테스트1|강조3|테스트3|
domain | 내용 |
---|---|
PF_INET, AF_INET | IPv4 인터넷 프로토콜을 사용합니다. |
PF_INET6 | IPv6 인터넷 프로토콜을 사용합니다. |
PF_LOCAL, AF_UNIX | 같은 시스템 내에서 프로세스 끼리 통신합니다. |
PF_PACKET | Low level socket 을 인터페이스를 이용합니다. |
PF_IPX | IPX 노벨 프로토콜을 사용합니다. |
type | 내용 |
---|---|
SOCK_STREAM | TCP/IP 프로토콜을 이용합니다. |
SOCK_DGRAM | UDP/IP 프로토콜을 이용합니다. |
-bind 함수는 주소정보를 앞에서 Socket()함수로 생성한 소켓에 할당하는 것이다.
struct sockaddr_in
{
sa_family_t sin_family; 주소체계(Address Family)
uint16_t sin_port; 16비트 TCP/UDP PORT번호
struct in_addr sin_addr; 32비트의 IP주소
char sin_zero[8]; 항상 0;
}
struct in_addr{
in_addr_t s_addr; 32비트의 IPv4 인터넷 주소가 담긴다.
}
struct sockaddr
{
sa_family_t sin_family 주소체계(Address Family)
char sa_data[14]; 주소정보
}
bind 함수의 2번째 인자 (struct sockaddr) *myaddr는
sockaddr 구조체로 캐스팅 된 내가 정의한 sockaddr_in 구조체의 주소이다.
위 코드블록의 구조체에 대한 분석을 보면 우리가 매개변수로 형변환해서 넘겨야할 구조체는 맨 아래에 있는 sockaddr 구조체인 것을 알 수 있다.
그 중 char sa_data[14]에는 bind 함수가 요구대로 IP(4byte)와 PORT(2byte)가 담겨지고 남은 부분은 0(8개 총 8byte)으로 채워야한다.
불편한 이 부분을 해결하기 위해 만들어진 구조체가 sockaddr_in(최상단) 구조체이며
1. sin_family->socket()에서 domain 인자로 들어갔던 값이 담긴다(ex: AF_INET...)
2. sin_port-> 포트번호를 빅엔디안(네트워크 바이트 순서)으로 저장
3. sin_addr-> 32비트 IP주소를 빅엔디안(네트워크 바이트 순서)으로 저장, in_addr 구조체는 속에 in_addr_t는 32비트 정수 자료형이다.
이 곳에는 자신의 IP를 할당하는데 자신의 랜카드가 2개 이상이여서 IP주소가 2개 이상 있지 않는다면 보통 INADDR_ANY를 사용하여 자동으로 할당되도록 할 수 있다.
4. sin_zero->sin_zero는 항상 0이어야 하는데 이를 어기면 간혹 IP주소를 터무니 없는 값으로 인식하는 경우가 생긴다 따라서 일반적으로 memset이나 zeroMemory 등으로 초기화한 후 사용한다.
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family=AF_INET;
servAddr.sin_addr.s_addr=htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi("8080"));
htonl,htons 의미:
htonl은 host to network long으로 이를 이해하기 앞서 빅엔디안과 리틀엔디안에 대해 알아야한다. 빅엔디안은 높은 메모리 주소에 뒤의 값을 넣는다. 예를 들자면 char *ch = "abcd";라는 문자열이 있을 경우 빅엔디안은 메모리가 낮은곳에서부터 a,b,c,d를 저장한다는 뜻이다. 물론 리틀 엔디안은 그 반대이다. 이들은 cpu가 데이터를 메모리에 저장하는 방식에 따라 다른게 되는데 네트워크 통신시에 데이터 전송을 정해놓지 않으면 cpu가 다르면 통신이 되지 않는 상황이 벌어질 수 있어 네트워크는 모두 빅 엔디안으로 통일하게 됐다. htonl,htons 함수는 호스트가 무엇이든 네트워크 즉 빅엔디안으로 long 또는 short 타입으로 변경한다는 뜻이다.
-주소가 할당된 소켓이 연결요청 대기상태로 들어간다.
연결 요청 대기 상태: 서버의 소켓과 큐가 완전히 준비되어 클라이언트의 연결 요청을 받아들일 수 있는 상태.
연결 요청 대기 큐: 연결 요청을 대기시킬 수 있는 일종의 대기실로 서버 측에서 accept 함수로 허락하기 전까지 머물러있다.
listen 함수 호출을 성공하게 되면, 이제 여러 클라이언트들이 연결을 요청해 올 것이고, 모든 연결 요청은 서버가 미리 만들어 놓은 대기실로 들어가 순서대로 연결요청이 수락될 때까지 기다려야 한다.
listen()함수를 호출하면 서버 소켓 상태는 CLOSE에서 LISTEN 상태로 변경되고, 연결을 요청한 클라이언트 소켓은 SYN_RCVD 상태에서 3-way-handshaking을 완료하고 ESTABLISHED 상태가 된다.
-대기상태의 클라이언트 요청을 수락한다.
int client_addr_size;
client_addr_size = sizeof( client_addr);
client_socket = accept( server_socket, (struct sockaddr*)&client_addr,
&client_addr_size);
if ( -1 == client_socket){
printf( "클라이언트 연결 수락 실패\n");
exit( 1);
}
sizeof()로 크기를 전달하던 bind와 차이점이다.
-데이터를 수신/송신한다.
(write)int fd 데이터를 보낼 소켓의 파일 디스크럽터(클라이언트)
(write)void *buf 전송할 데이터를 저장할 버퍼의 주소 값 전달
(write)size_t nbytes 전송할 데이터의 바이트 수 전달
인자1. fd
read : 데이터를 받을 대상 소켓의 파일 디스크립터 (클라이언트)
write : 데이터를 보낼 대상 소켓의 파일 디스크립터 (클라이언트)
인자2. buf
read : 데이터를 받을 버퍼의 주소 값
write : 데이터를 보낼 버퍼의 주소 값
인자3. nbytes
read : 수신할 최대 바이트 수 전달
write : 송신할 데이터의 바이트 수 전달
-연결된 소켓을 종료한다.
서버와 connect 함수만 다르고 다 서버에서 정리한 내용이라 connect 함수만 다뤄보겠다.
-listen 중인 서버 소켓에 연결요청을 한다.
connect()가 성공적으로 끝나면 바로 read/write 가능