본 글은 윤성우의 TCP/IP 소켓 프로그래밍을 참고하였습니다

순서

I. 소켓 프로그래밍 절차

II. 리눅스와 윈도우의 구현 차이점

III. 서버 호출 함수 정보

WSAStartup()
- socket() - bind()- listen() - accept() - recv()/send() - closesocket() - WSACleanup() 

IV. 서버 전체 구현 코드


I. 소켓 프로그래밍 절차 (Window)

Window용 서버와 클라이언트의 함수 호출 순서

 


II. 리눅스와 윈도우의 구현 차이점

Window 소켓 프로그래밍의 절차는 리눅스와 다른 점이 있는데,

위 절차의 보라색 글씨가 그 부분이다.

1. <winsock2.h> 헤더 사용
    리눅스에서는 sock()의 반환 형이 소켓의 "파일 디스크립터"(int)였다면,
    윈도우에서는 "소켓 핸들" (SOCKET)을 반환한다.

2. WSAStartup() 함수와 WSACleanup() 함수의 사용
    윈도우에서는 반드시 프로그램에서 요구하는 윈도우 소켓 버전을 알리고 
    라이브러리를 초기화 하는 작업을 진행해야 한다.

3. read,write 대신 recv,send 함수 사용
    함수 이름 뿐 아니라 윈도우에서는 3번째 인자로 옵션도 추가할 수 있다.

4. close 대신 closesocket 함수 사용
리눅스와 같은 파일 디스크립터가 아닌 소켓 핸들이므로 closesocket으로 소켓 핸들을 닫는다.


 

III. 서버 호출 함수 정보

#include <sys/socket.h>

1. WSAStartup() 함수

프로그램에서 사용할 윈속의 버전정보를 통해 라이브러리를 초기화한다.

#include <winsock2.h>

int WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData);
    --> 성공 시 0, 실패 시 0이 아닌 에러코드 값 반환
    ㄴ. wVersionRequested : 사용할 윈속의 버전정보 전달
    ㄴ. lpWSAData         : WSADATA라는 구조체 변수의 주소 값 전달.


WSADATA userwsaData; // WSADATA 변수 선언

WSAStartupret = WSAStartup(MAKEWORD(1, 2), &userwsaData); 
    -> 주 버전1 부 버전 2 (1.2)
    -> MAKEWORD는 0x0201을 반환해준다.
더보기

인자1. WORD wVersionRequested
사용할 윈속 버전 정보

사용할 윈속의 버전 정보를 전달해야한다.

만약 주 버전1, 부 버전 2라면 0x0201을 인자로 넣어줘야 한다. 너무 불편하다.

따라서 윈도우에서는 MAKEWORD()라는 함수를 반환하는데 이를 통해 알아서 변환시켜준다
MAKEWORD(1,2) = 0x0201 = 1.2버전

인자2. LPWSADATA lpWSAData
WSADATA 구조체 변수의 주소 값

자료형인 LPWSADATA는 WSADATA의 포인터 형이다
( LPWSADATA = WSADATA * )

WSADATA변수는 위 코드처럼 WSADATA userwsaData; 로 정의하면 된다.

이 함수의 호출이 끝나면 해당 변수로 초기화 된 라이브러리의 정보가 채워진다.

 


2. socket()함수

- socket 함수는 어떤 프로토콜을 가진 소켓으로 통신할 것인지 결정하여 생성하는 함수이다.

#include <winsock2.h>

SOCKET socket( int af, int type, int protocol );
	--> 성공 시 소켓핸들, 실패시 INVALID_SOCKET 반환.
    ㄴ. af       : 소켓이 사용할 프로토콜 체계(Protocol Family) 정보 전달 = domain
    ㄴ. type     : 소켓의 데이터 전송방식에 대한 정보 전달
    ㄴ. protocol : 두 컴퓨터간 통신에 사용되는 프로토콜 정보 전달

"IPv4 인터넷 프로토콜 체계에서 동작하는 연결지향형 데이터 전송 소켓"
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

"IPv4 인터넷 프로토콜 체계에서 동작하는 비 연결지향형 데이터 전송 소켓"
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
더보기

인자1. int af
프로토콜 체계

우리는 주로 IPv4를 사용한다. 
IPv6은 IP주소가 모두 고갈될 것을 염려하여 만들어진 16byte 주소 표준이다.

(+ 주소 표준이란?)

주로 사용하는 IPv4 (4byte)기준으로 ABCD 4개의 주소 클래스가 있는데,

차례대로 네트워크 ID의 Byte크기가 1,2,3,4이고
호스트 ID의 Byte크기가 3,2,1,0 이다.

이 주소 클래스에 따라 127.x.x.x 인지, 192.x.x.x인지 첫 번째 바이트 범위가 나누어진다.
A : 0~127
B: 128~191
C : 192~223

<sys/socket.h>에 포함된 프로토콜 체계

우리는 주로 IPv4를 사용한다. 
IPv6은 IP주소가 모두 고갈될 것을 염려하여 만들어진 16byte 주소 표준이다.

(+ 주소 표준이란?)

주로 사용하는 IPv4 (4byte)기준으로 ABCD 4개의 주소 클래스가 있는데,

차례대로 네트워크 ID의 Byte크기가 1,2,3,4이고
호스트 ID의 Byte크기가 3,2,1,0 이다.

이 주소 클래스에 따라 127.x.x.x 인지, 192.x.x.x인지 첫 번째 바이트 범위가 나누어진다.
A : 0~127
B: 128~191
C : 192~223

인자2. int type
전송방식

위 II - 3에서 설명한 TCP/UDP 계층을 선택한다.

TCP : SOCK_STREAM 
UDP : SOCK_DGRAM

인자3. int protocol
프로토콜 정보

하나의 프로토콜 체계 안에 데이터의 전송방식이 동일한 프로토콜이 둘 이상 존재할 때 필요한 인자이다.

말이 길고 어려운데, 우리가 원하는 TCP, UDP를 사용할 때는 0을 넣어도 무관하다. (만족하는 것이 딱 1개 이므로)

 


3. bind() 함수

- bind()함수는 주소정보를 앞서 생성한 소켓에 할당하는 것이다.

#include <winsock2.h>
#include <string.h>

int bind(SOCKET s, (const struct sockaddr) *name, int namelen);
    --> 성공 시 0, 실패 시 SOCKET_ERROR 반환
    ㄴ. s       : 주소정보(IP,PORT)할당 할 소켓 변수 (sock()의 반환 값)
    ㄴ. name    : 할당 하고자하는 주소정보를 지니는 "구조체 변수의 주소 값"
    ㄴ. namelen : name 구조체 변수의 길이정보.


WSADATA userwsaData;
SOCKET hServSock;
SOCKADDR_IN serv_addr;

char*[] serv_port = "10000";

WSAStartup(MAKEWORD(2,2), &userwsaData);

// sock()함수 사용해 서버의 소켓 핸들.
hServSock = socket(PF_INET, SOCK_STREAM, 0)

// 구조체 주소정보 초기화 ** 이부분 개념 중요. 글 하단 설명 참고
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(serv_port) );

// bind() (SOCKADDR*)로 캐스팅*
bind(hServSock, (SOCKADDR*) &serv_addr, sizeof(serv_addr));
구조체에 대한 분석

struct sockaddr_in 은 SOCKADDR_IN와 같다

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]; 사용되지 않음 -> ??
}

struct in_addr{
    in_addr_t    s_addr;  32비트의 IPv4 인터넷 주소가 담긴다.
}

struct sockaddr 은 SOCKADDR과 같다
struct sockaddr
{
    sa_family_t sin_family    주소체계(Address Family)
    char        sa_data[14];  주소정보
}
더보기

인자1. int s

소켓 핸들

sock()함수를 통해 우리가 원하는 프로토콜을 가진 소켓 핸들이다.

인자2. (struct sockaddr) *name
*sockaddr 구조체로 캐스팅 된 내가 정의한 sockaddr_in 구조체 주소 

우선 위 코드블럭의 "구조체에 대한 분석"을 보자"

참고!! : 리눅스의 struct sockaddr = 윈도우의 SOCKADDR
리눅스의 struct sockaddr_in = 윈도우의 SOCKADDR_IN

우선, 우리는 결과적으로 3번째 구조체인 sockaddr을 함수 인자로 보내야 한다.그 중 char sa_data[14]에는 bind함수가 요구대로
IP(4byte)PORT(2byte)가 모두 담겨지고 남은 부분은 또 0(8개 총 8byte)으로 채워야한다. 

이것은 매우 불편한 상황인데 이를 해결해 주는 구조체가 sockaddr_in 구조체이다!!

 

이 불편함을 해결해 줄 sockaddr_in의 멤버들을 보자.

1. sin_family -> sock()에서 인자로 넣었던 주소 체계로 AF_INET등 이 담긴다.

2. sin_port   -> 포트번호를 "네트워크 바이트 순서"로 저장해야함!!!

3. sin_addr   -> 32비트 IP주소를 "네트워크 바이트 순서"로 저장해야함!!
   그 속의 in_addr_t는 32비트 정수자료형임.
  
4. sin_zero[8] -> (struct sockaddr*)로 캐스팅 시 바이트 열 맞추기 위함. 0으로 채워짐

이 구조체의 2,3,4는 sockaddr구조체의 sa_data를 3개로 쪼개 놓아서 편하게 정의할 수 있도록 해준다!!
이 용도의 구조체이며, sin_zero를 위해 처음에 0으로 memset()을 해준다.

인자3. int namerlen
인자2에 넣어준 구조체 변수의 길이정보 (sizeof)

 sizeof(name)

 


4. listen() 함수

- 주소가 할당된 소켓이 연결요청 대기상태로 들어간다.

#include <winsock2.h>

int listen(SOCKET s, int backlog);
    --> 성공 시 0, 실패 시 SOCKET_ERROR 반환
    ㄴ. s       : 연결요청 대기상태로 두고자 하는 소켓 핸들
                  이 인자의 소켓 핸들이 서버 소켓이 된다.
    ㄴ. backlog : 연결요청 대기 큐(Queue)의 크기정보 전달.
                  이 Queue의 크기만큼 클라이언트의 연결요청을 대기시킬 수 있다.
더보기

인자1. SOCKET s
서버의 소켓의 핸들

이 인자는 우리가 앞서 생성하고 주소할당을 한 소켓 핸들이다.

이 소켓 핸들이 서버 소켓(리스닝 소켓)이 된다.

인자2. int backlog
연결요청 대기 큐(Queue)의 크기

"은행 창구"와 같다.

모든 클라이언트의 요청은 순서대로 이루어지는데, 이미 창구가 사용 중 이면 (server-client 연결 중)
이후 클라이언트 들은 대기표를 뽑고 대기하고 있는 것이다.
이 대기창구를 시스템에서는 Queue에 저장하고 있다.

따라서 이 인자는 최대 몇 개의 클라이언트를 대기상태로 둘 것인지 정하는 숫자가 된다. 

 


5. accept() 함수

 - 대기상태의 클라이언트 요청을 수락한다.

#include <winsock2.h>

SOCKET accept(SOCKET s, (struct sockaddr*) addr, int* addrlen)
    --> 성공 시 생성된 소켓 핸들, 실패 시 INVALID_SOCKET 반환
    ㄴ. s       : 서버 소켓의 소켓 핸들
    ㄴ. addr    : 연결요청 한 클라이언트의 주소정보를 담을 변수의 주소 값.
    ㄴ. addrlen : 두 번째 인자로 전달된 addr의 크기정보를 전달.
                  단! 미리 변수에 크기정보를 저장한 뒤, 주소를 전달한다. -> bind와 다른점
더보기

인자1. SOCKET s
서버 소켓의 소켓 핸들을 전달한다.

인자2. addr
클라이언트 연결요청이 오면 그 주소정보를 담는 변수이다.

bind() 함수에서는 "서버"소켓에 주소정보를 담기위해 직접 멤버에 접근해 프로토콜을 지정해주었다.

하지만 accept() 함수에서는 struct sockaddr_in 구조체만 정의해주고 그 변수만 인자로 넣어주면 된다.
(이 변수에 클라이언트 주소정보가 담긴다)

이 변수 또한 struct sockaddr_in으로 정의해서 (struct sockaddr*)로 캐스팅 한다.

이 부분도 위 bind와 마찬가지로 윈도우에서는 SOCKADDR_IN과 SOCKADDR*로 된다.

인자3. int* addrlen
인자2의 구조체의 길이정보를 담고 있는 주소 

핵심은 길이정보를 이미 담고 있어야 한다.

bind()에서는 인자 자체에 크기 변수가 들어갔는데,
accept()에서는 크기 정보가 들어가 있는 변수의 주소이다.

위와 같은 차이점에 주의하자.

 


6.recv()/send()

데이터를 수신/송신한다.

#include <winsock2.h>

int recv(SOCKET s, const char* buf, int len, int flags);
    -> 성공 시 수신한 byte 수(단 EOF 전송 시 0), 실패 시 SOCKET_ERROR 반환
    ㄴ. s      : 데이터 수신 대상과의 연결을 의미하는 소켓의 핸들 값 전달.
    ㄴ. buf    : 수신한 데이터를 저장할 버퍼의 주소 값 전달
    ㄴ. len    : 수신할 최대 바이트 수 전달.
    ㄴ. flasg  : 데이터 수신 시 적용할 다양한 옵션 정보 전달
    
int  send(SOCKET s, const char *buf, int len, int flags);
    --> 성공 시 전송된 byte 수, 실패 시 SOCKET_ERROR 반환.
    ㄴ. s      : 데이터 전송 대상과의 연결을 의미하는 소켓의 핸들 값 전달.
    ㄴ. buf    : 전송할 데이터가 저장된 버퍼의 주소 값 전달
    ㄴ. nbytes : 전송할 데이터의 바이트 수 전달.
    ㄴ. flags  : 데이터 전송 시 적용할 다양한 옵션 정보 전달



SOCKET hClntSock; //파일 디스크립터
char read_message[BUF_SIZE];
char send_message[BUF_SIZE];
int str_len;

str_len = recv(hClntSock, read_message, sizeof(read_message) -1, 0);

send(hClntSock, send_message, sizeof(send_message), 0); 
더보기

인자1. SOCKET s
recv : 데이터를 받을 대상 소켓의 파일 디스크립터 (클라이언트)
send : 데이터를 보낼 대상 소켓의 파일 디스크립터 (클라이언트)

인자2. const char* buf
recv : 데이터를 받을 버퍼의 주소 값
send : 데이터를 보낼 버퍼의 주소 값  

인자3. int len
recv : 수신할 최대 바이트 수 전달
send : 송신할 데이터의 바이트 수 전달

인자4. int flags
다양한 옵션들.. 없으면 0

 


7. close()

연결된 소켓을 종료한다.

#include <winsock2.h>

int closesocket(SOCKET s);
    --> 성공 시 0, 실패 시 SOCKET_ERROR 반환
    ㄴ. s : 닫고자 하는 소켓 핸들

 

8. WSACleanup()

할당 했던 윈속 라이브러리를 윈도우 운영체제에 반환한다.

#include <winsock2.h>

int WSACleanup(void);
    -> 성공 시 0, 실패 시 SOCKET_ERROR 반환

 


III. 서버 전체 구현 코드

 

// 에러 핸들링은 제외 함.
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

int main()
{
    int port = 10000;
    // 변수 선언
    
    WSADATA wsaData;
    SOCKET hServSock, hClntSock;
    SOCKADDR_IN servAddr, clntAddr;
    
    char sendmsg[] = "SendTest";
    
    // 라이브러리 사용 확인
    WSAStartup(MAKEWORD(2,2), &wsaData);
    
    // 서버 소켓 생성
    hServSock = socket(PF_INET, SOCK_STREAM, 0);
    
    // 서버 소켓에 넣을 주소 구조체 생성
    memset(&servAddr, 0 ,sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port=htons(port);
    
    // 서버 소켓에 주소 할당
    bind(hServSock, (SOCKADDR*) &servAddr, sizeof(servAddr));
    
    // 서버 소켓에 들어오는 요청 대기
    listen(hServSock, 10);
    
    // 클라이언트 주소 구조체 크기변수 정의
    int size_clntAddr = sizeof(clntAddr);
    
    // 클라이언트 소켓 핸들에 승인된 클라이언트 주소 할당
    hClntSock = accept(hServSock, (SOCKADDR*) &clntAddr, &size_clntAddr);
    
    // 할당 된 클라이언트에 메시지 보냄.
    send(hClntSock, sendmsg, sizeof(sendmsg), 0);
    
    // 소켓닫기
    closesocket(hClntSock);
    closesocket(hServSock);
    
    // 라이브러리 사용 끝나서 반환
    WSACleanup();
    
    return 0;
 }

+ Recent posts