reference: "TCP/IP 소켓 프로그래밍" / 김선우 / 한빛아카데미
TCP 클라이언트가 보내준 데이터를 화면에 표시하는 TCP 서버 구현 예제이다.
/*
* TCP/IP 소켓 프로그래밍 템플릿
*/
#define _CRT_SECURE_NO_WARNINGS // 구형 C 함수 사용 시 경고 끄기
#define _WINSOCK_DEPRECATED_NO_WARNINGS // 구형 소켓 API 사용 시 경고 끄기
// 윈속2 메인 헤더
#include <WinSock2.h>
// 윈속2 확장 헤더
#include <WS2tcpip.h>
#include <tchar.h> // _T(), ...
#include <stdio.h> // printf(), ...
#include <stdlib.h> // exit(), ...
#include <string.h> // strncpy(), ...
// ws2_32.lib 링크
#pragma comment(lib, "ws2_32")
// 소켓 함수 오류 출력 후 종료
// msg 인수로 전달된 문자열과 더불어 현재 발생한 오류 메시지를 화면에 메시지 상자로 표시하고,
// 응용 프로그램 종료
void err_quit(const char* msg)
{
LPVOID lpMsgBuf;
// 소켓 함수의 리턴값으로 오류 발생이 확인되었다면,
// WSAGetLastError()가 리턴해주는 오류 코드에 대응하는 오류 메시지를
// FormatMessage() 함수로 얻을 수 있다.
FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(char*)&lpMsgBuf, 0, NULL);
MessageBoxA(NULL, (const char*)lpMsgBuf, msg, MB_ICONERROR);
LocalFree(lpMsgBuf);
exit(1);
}
// 소켓 함수 오류 출력
// err_quit 함수에서는 출력함수로 MessageBox() 함수 대신 printf()를 사용
// 그리고 exit(1) 제거
// 오류 메시지를 출력하되 응용 프로그램을 종료하지는 않는다.
void err_display(const char* msg)
{
LPVOID lpMsgBuf;
FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(char*)&lpMsgBuf, 0, NULL);
printf("[%s] %s\n", msg, (char*)lpMsgBuf);
LocalFree(lpMsgBuf);
}
// 소켓 함수 오류 출력
void err_display(int errcode)
{
LPVOID lpMsgBuf;
FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, errcode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(char*)&lpMsgBuf, 0, NULL);
printf("[오류] %s\n", (char*)lpMsgBuf);
LocalFree(lpMsgBuf);
}
#include "Common.h"
#define SERVERPORT 9000
#define BUFSIZE 512
// TCP 서버(IPv4)
DWORD WINAPI TCPServer4(LPVOID arg) { // DWORD = 4byte = 32bit
int retval;
// 소켓 생성
// (주소 체계, 소켓 타입, 사용할 프로토콜)
SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock == INVALID_SOCKET)
err_quit("socket()");
// (주소 체계 상수값)
// AF_INET: IPv4
// AF_INET6: IPv6
// AF_BTH: 블루투스
//
// (소켓 타입): 프로토콜 특성을 나타냄
// SOCK_STREAM: 사용할 프로토콜이 TCP인 경우
// SOCK_DGRAM: 사용할 프로토콜이 UDP인 경우
//
// (프토토콜)
// TCP나 UDP 프로토콜은 '주소 체계'와 '소켓 타입'만으로 프로토콜을 결정할 수 있으므로,
// 대개는 프로토콜 부분에 0을 넣는다.
// bind()
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET; // 주소 체계
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(SERVERPORT);
retval = bind(listen_sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (retval == SOCKET_ERROR) err_quit("bind()");
// listen()
retval = listen(listen_sock, SOMAXCONN);
if (retval == SOCKET_ERROR) err_quit("listen()");
// 데이터 통신에 사용할 변수
SOCKET client_sock;
struct sockaddr_in clientaddr;
int addrlen;
char buf[BUFSIZE + 1];
while (1) {
// accept()
addrlen = sizeof(clientaddr);
client_sock = accept(listen_sock, (struct sockaddr*)&clientaddr, &addrlen);
if (client_sock == INVALID_SOCKET) {
err_display("accept()");
break;
}
// 접속한 클라이언트 정보 출력
printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
// 클라이언트와 데이터 통신
while (1) {
// 데이터 받기
retval = recv(client_sock, buf, BUFSIZE, 0);
if (retval == SOCKET_ERROR) {
err_display("recv()");
break;
}
else if (retval == 0)
break;
// 받은 데이터 출력
buf[retval] = '\0';
printf("%s", buf);
}
// 소켓 닫기
closesocket(client_sock);
printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
}
// 소켓 닫기
closesocket(listen_sock);
//소켓을 사용한 통신을 마치면 관련 리소스를 반환해야 함.
// closesocket() 함수를 통해 관련 리소스를 운영체제에 반환
return 0;
}
// TCP 서버(IPv6)
DWORD WINAPI TCPServer6(LPVOID arg)
{
int retval;
// 소켓 생성
SOCKET listen_sock = socket(AF_INET6, SOCK_STREAM, 0);
if (listen_sock == INVALID_SOCKET) err_quit("socket()");
// 듀얼 스택을 끈다. [Windows는 꺼져 있음(기본값). UNIX/Linux는 OS마다 다름]
int no = 1;
setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&no, sizeof(no));
// bind()
struct sockaddr_in6 serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin6_family = AF_INET6;
serveraddr.sin6_addr = in6addr_any;
serveraddr.sin6_port = htons(SERVERPORT);
retval = bind(listen_sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (retval == SOCKET_ERROR) err_quit("bind()");
// listen()
retval = listen(listen_sock, SOMAXCONN);
if (retval == SOCKET_ERROR) err_quit("listen()");
// 데이터 통신에 사용할 변수
SOCKET client_sock;
struct sockaddr_in6 clientaddr;
int addrlen;
char buf[BUFSIZE + 1];
while (1) {
// accept()
addrlen = sizeof(clientaddr);
client_sock = accept(listen_sock, (struct sockaddr*)&clientaddr, &addrlen);
if (client_sock == INVALID_SOCKET) {
err_display("accept()");
break;
}
// 접속한 클라이언트 정보 출력
char ipaddr[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &clientaddr.sin6_addr, ipaddr, sizeof(ipaddr));
printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n",
ipaddr, ntohs(clientaddr.sin6_port));
// 클라이언트와 데이터 통신
while (1) {
// 데이터 받기
retval = recv(client_sock, buf, BUFSIZE, 0);
if (retval == SOCKET_ERROR) {
err_display("recv()");
break;
}
else if (retval == 0)
break;
// 받은 데이터 출력
buf[retval] = '\0';
printf("%s", buf);
}
// 소켓 닫기
closesocket(client_sock);
printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n",
ipaddr, ntohs(clientaddr.sin6_port));
}
// 소켓 닫기
closesocket(listen_sock);
return 0;
}
int main(int argc, char* argv[])
{
// 윈도우에서는 소켓 생성 전에 윈속 초기화와
// 소켓 닫기 후 윈속 종료 단계가 더 필요함
// 즉 모든 윈속 프로그램은 최초 소켓 함수를 호출하기 전에 반드시 윈속 초기화 함수인
// WSAStartup() 함수를 호출해야 함.
// 이 함수는 프로그램에서 사용할 윈속 버전을 요청하여 윈속 라이브러리(WS2_32.DLL)를 초기화한다.
// 이 함수가 실패하면 DLL이 메모리에 로드되지 않는다.
// 윈속 초기화
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return 1;
// WSADATA 구조체를 전달하면 이를 통해 윈도우 운영체제가 제공하는 윈속 구현에 관한 정보를 얻을 수 있다.
// 멀티스레드를 이용하여 두 개의 서버를 동시에 구동한다.
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, TCPServer4, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, TCPServer6, NULL, 0, NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
// 윈속 종료
WSACleanup();
// 프로그램을 종료할 떄는 윈속 종료 함수인 WSACleanup() 함수를 호출해야 한다.
// 이 함수는 윈속 사용의 중지를 운영체제에 알려서 관련 리소스를 반환하는 역할을 한다.
return 0;
}
텔넷(telnet) 클라이언트(윈도우 OS에 내장)를 사용하여 구현한 TCP 서버 프로그램을 테스트할 수 있다.
같은 PC에서 테스트하기에 IPv4 루프백 주소인 127.0.0.1을 사용한다.
다음과 같이 루프백 주소와 9000번 포트와 연결된 응용 프로그램, 즉 구현한 TCP 서버 프로그램과 연결한다.
연결 후 TCP 서버 프로그램의 콘솔에 연결 정보가 뜬다.
텔넷 클라이언트에 문자열을 입력하면 TCP 서버 콘솔에 뜨는 것을 확인할 수 있다.