오늘은 지난 포스트에 이어서 Winsock을 사용하여 간단한 server를 만들어보려고 한다.
지난 포스트에서 서버는 아래와 같은 절차가 있다고 언급했다.
- Winsock을 초기화합니다.
- 소켓을 만듭니다.
- 소켓을 바인딩합니다.
- 클라이언트에 대한 소켓에서 수신 대기합니다.
- 클라이언트에서 연결을 수락합니다.
- 데이터를 수신하고 보냅니다.
- 연결 끊기를 선택합니다.
위와 같은 순서로 한번 만들어 보려고 한다.
Winsock을 초기화합니다.
우선 헤더 파일 및 lib 파일과 연결한다.
Winsock 초기화에 대한 부분은 저번 포스트에서 다뤘으니 넘어가도록 하겠다.
#include <stdio.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
/*...*/
WSACleanup();
}
소켓을 만듭니다.
서버가 클라이언트 연결을 수신 대기할 수 있도록 listenSocket이라는 SOCKET 개체를 만든다.
socket 함수의 원형은 아래와 같다.
SOCKET WSAAPI socket( [in] int af, [in] int type, [in] int protocol );
af(address family specification)에 들어가는 값은 주로 AF_INET,(IPv4) AF_INET6(IPv6) 이다.
type에 들어가는 값은 소켓의 Type을 결정하게 되는데, 각각의 설명은 아래와 같다.
SOCK_STREAM 데이터 중복이나 경계 유지 없이 신뢰성 있는 양방향 연결 기반의 바이트 스트림을 지원합니다. 이 종류의 Socket은 단일 피어와 통신하며 이 소켓을 사용할 경우 통신을 시작하기 전에 원격 호스트에 연결해야 합니다. Stream은 Transmission Control Protocol(ProtocolType.Tcp) 및 AddressFamily.InterNetwork 주소 패밀리를 사용합니다.
SOCK_DGRAM의 경우 고정된 최대 길이(대개 작음)의 신뢰할 수 없고 연결 없는 메시지인 데이터그램을 지원합니다. 메시지가 손실되거나 중복될 수 있으며 메시지 순서가 잘못될 수도 있습니다. Socket 종류의 Dgram은 데이터를 보내고 받기 전에 연결하지 않고도 여러 피어와 통신할 수 있습니다. Dgram은 Datagram Protocol(ProtocolType.Udp)과 AddressFamily.InterNetwork 주소 패밀리를 사용합니다.
즉, 간단하게 설명하자면 TCP - SOCK_STREAM, UDP - SOCK_DGRAM이라고 설명 할 수있다.
protocol에는 어떤 프로토콜을 사용할 것인지에 대한 인자이다.
주로, TCP - IPPROTO_TCP, UPD - IPPROTO_UDP 이런식으로 들어가게 되지만,
사실상 type으로 거의 결정이 되므로, 0을 넣으면 type에 맞는 값으로 자동으로 설정된다.
이후 Listen 소켓의 정보를 저장해주면 된다.
SOCKADDR_IN 구조체를 사용한다.
inet_pton 함수를 이용하여 서버의 주소를 저장한다.
sin_family에는 IPv4인지 IPv6인지에 대한 값을 넣어준다. socket함수의 첫번째 인자로 들어가는 값과 같다.
sin_port는 몇번 포트를 할당 할 것인지에 대한 내용이다.
사용 가능한 Port 범위는 0 ~ 65535번까지 사용 가능하다(unsigned short)
0 ~ 1023까지는 well-known port 영역으로 예약 영역이라고 볼 수 있다.
1024~49151번까지는 등록된 포트 (registered port)이고, 주로 서버 소켓으로 사용하는 영역이다.
49152 ~ 65535번까지는 동적 포트로 client에서 자동으로 할당되는 영역이다.
따라서 우리는 1024~49151중에서 하나 선택해서 사용하면 되겠다.
/*...*/
SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN serverSockAddr;
inet_pton(AF_INET, "127.0.0.1" , &serverSockAddr.sin_addr);
serverSockAddr.sin_family = AF_INET;
serverSockAddr.sin_port = htons(7777);
/*...*/
소켓을 바인딩합니다.
bind 함수를 통해 소켓을 바인딩 한다.
들어가는 인자는 listen소켓, 위에서 저장한 소켓 정보 구조체, 해당 구조체 크기를 입력해야 한다.
SOCKADDR*로 형변환 하는 이유는 확장성때문이라고 한다.
우리가 사용하고 있는 SOCKADDR_IN은 IPv4인 환경에서만 사용되기 때문에 혹여나 다른 IPv6나 다른 주소체계일 경우에도 사용 가능하도록 SOCKADDR*로 형변환 하는것이라고 한다.
/*...*/
if (::bind(listenSocket, reinterpret_cast<SOCKADDR*>(&serverSockAddr), sizeof(serverSockAddr)) == SOCKET_ERROR) {
wprintf(L"bind failed with error: %ld\n", WSAGetLastError());
}
/*...*/
클라이언트에 대한 소켓에서 수신 대기합니다.
이제 Listen 함수를 호출하므로써 실제로 클라이언트 접속을 처리할 준비가 됐다.
listen 함수에 listen소켓을 넣어주고, backlog를 넣어주는데 backlog에 넣어주는 값은 입력 요청을 받았을때 대기할 수 있는 큐의 최대값이다.
/*...*/
if (listen(listenSocket, 10) == SOCKET_ERROR) {
wprintf(L"listen failed with error: %ld\n", WSAGetLastError());
}
/*...*/
클라이언트에서 연결을 수락합니다.
accept 함수를 통해 클라이언트의 연결 요청을 수락한다.
연결된 클라이언트의 정보를 얻기 위해 clientSockAddr이라는 SOCKADDR_IN 구조체를 하나 만든다.
clientSockSize는 해당 구조체의 크기를 저장한다.
accept함수에 listen소켓, clientSockAddr, clientSockSize를 넣고 호출하고 문제가 없다면 연결 요청한 client 소켓을 반환한다.
반환된 socket을 통해 데이터를 송수신 할수있다.
/*...*/
SOCKADDR_IN clientSockAddr;
int clientSockSize = sizeof(clientSockAddr);
memset(&clientSockAddr, 0, clientSockSize);
SOCKET clientSock = accept(socket, reinterpret_cast<SOCKADDR*>(&clientSockAddr), &clientSockSize);
if (clientSock == INVALID_SOCKET) {
wprintf(L"accept failed with error: %ld\n", WSAGetLastError());
}
else
wprintf(L"Client connected.\n");
/*...*/
데이터를 수신하고 보냅니다.
recv 함수를 통해 위에서 연결된 소켓에서 데이터를 recv한다.
send 함수를 통해 받은 데이터를 그대로 다시 client에 전달한다.
위 두 함수에 들어가는 인자들은 동일하다. 마지막에 0으로 설정한 부분은 flag로 OOB관련 설정을 할때 필요하다.
/*...*/
while (1)
{
char recvBuff[100];
memset(recvBuff, 0, sizeof(recvBuff));
int len = recv(clientSock, recvBuff, 100, 0);
printf("recv data = %s\n", recvBuff);
send(clientSock, recvBuff, len, 0);
}
/*...*/
이렇게 서버 코드를 한번 훑어봤다.
아래는 전체 소스 코드이다.
#include <stdio.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
SOCKET listenSock = ::socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN serverSockAddr;
inet_pton(AF_INET, "127.0.0.1", &serverSockAddr.sin_addr);
serverSockAddr.sin_family = AF_INET;
serverSockAddr.sin_port = htons(7777);
if (::bind(listenSock, reinterpret_cast<SOCKADDR*>(&serverSockAddr), sizeof(serverSockAddr)) == SOCKET_ERROR) {
wprintf(L"bind failed with error: %ld\n", WSAGetLastError());
}
if (listen(listenSock, 10) == SOCKET_ERROR) {
wprintf(L"listen failed with error: %ld\n", WSAGetLastError());
}
SOCKADDR_IN clientSockAddr;
int clientSockSize = sizeof(clientSockAddr);
memset(&clientSockAddr, 0, clientSockSize);
SOCKET clientSock = accept(listenSock, reinterpret_cast<SOCKADDR*>(&clientSockAddr), &clientSockSize);
if (clientSock == INVALID_SOCKET) {
wprintf(L"accept failed with error: %ld\n", WSAGetLastError());
}
else
wprintf(L"Client connected.\n");
while (1)
{
char recvBuff[100];
memset(recvBuff, 0, sizeof(recvBuff));
int len = recv(clientSock, recvBuff, 100, 0);
printf("recv data = %s\n", recvBuff);
send(clientSock, recvBuff, len, 0);
}
WSACleanup();
}
'Development > Network' 카테고리의 다른 글
멀티프로세싱, 멀티쓰레딩, 멀티플렉싱 (1) | 2022.10.03 |
---|---|
[Windows Socket] - Client (0) | 2022.09.08 |
[Windows Socket] - 시작하기 (0) | 2022.09.08 |