본문 바로가기

Networking

[Socket Programming #1] Server/Client based on TCP

 

Socket Programing을 통해 Server와 Client가 연결을 맺는 과정에서 Server 쪽 TCP State가 어떻게 변화하는지 알아보도록 하겠습니다.

 

TCP Server 함수호출 순서

socket()    > bind()                > listen()                    > accept()   > read()/write()   > close()

소켓 생성 > 소켓 주소 할당 > 연결요청 대기상태 > 연결허용 > 데이터 송수신 > 연결종료

 

1. 가장 먼저 socket() 함수를 통해 socket 생성 ## 이 때 생성되는 socket은 client로부터 connection 요청을 받기 위한 socket으로 client와 데이터 송수신을 위해서는 별도의 socket이 필요

2. 주소 정보를 담기 위한 SOCKADDR_IN type의 변수를 초기화한 후, bind() 함수를 이용하여 이전에 생성한 socket에 할당

3. 다음으로 listen() 함수를 호출하여 client가 connect() 함수를 이용하여 연결 요청 가능한 상태로 전환 ## 즉, listen 함수가 호출되어야 client가 연결을 시도할 수 있음

4. listen() 함수의 두 번째 매개변수인 backlog는 client로부터 얼마나 많은 연결 요청을 대기 상태로 유지할 수 있는지를 결정하는 queue의 크기

5. 이제 client로부터 연결 요청을 받을 수 있는 상태는 되었으나, client와 데이터 송수신을 위해서는 위에서 설명한 대로 별도의 socket이 필요하며 이는 accept() 함수의 return 값을 이용

6. accept() 함수는 연결요청 대기 queue에 있는 client의 연결요청을 수락하는 함수로, 함수 호출이 정상적으로 성공하 데이터 송수신에 필요한 socket을 반환 ## 생성된 socket은 자동으로 연결 요청을 한 client의 socket과 연결

7. 이 후에는 데이터 송수신 처리

 

Server 쪽 Code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
 
#pragma comment (lib, "Ws2_32.lib")
 
#define BUF_SIZE 1024
void ErrorHandling(char* message);
 
int main(int argc, char* argv[]) {
 
    WSADATA wsaData;
    SOCKET hServSock, hClntSock;
    char message[BUF_SIZE];
    int strLen, i;
 
    SOCKADDR_IN servAddr = { 0 }, clntAddr = { 0 };
    int clntAddrSize;
 
    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
 
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");
 
    hServSock = socket(PF_INET, SOCK_STREAM, 0);
    if (hServSock == INVALID_SOCKET)
        ErrorHandling("socket() error!");
 
    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(atoi(argv[1]));
 
    if(bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr))==SOCKET_ERROR)
        ErrorHandling("bind() error!");
 
    if (listen(hServSock, 5) == SOCKET_ERROR)
        ErrorHandling("listen() error!");
 
    clntAddrSize = sizeof(clntAddr);
 
    for (i = 0; i < 5; i++) {
        hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &clntAddrSize);
        if (hClntSock == -1)
            ErrorHandling("accept() error");
        else
            printf("Connected client %d \n", i + 1);
 
        while ((strLen = recv(hClntSock, message, BUF_SIZE, 0)) != 0)
            send(hClntSock, message, strLen, 0);
 
        closesocket(hClntSock);
    }
    closesocket(hServSock);
    WSACleanup();
    return 0;
}
 
void ErrorHandling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

 

TCP Client 함수호출 순서

Socket()     > connect()    > read() / write()    > close()

소켓생성   > 연결요청     > 데이터 송수신    > 연결종료

 

Client 쪽 Code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
 
#pragma comment (lib, "Ws2_32.lib")
 
#define BUF_SIZE 1024
void ErrorHandling(char* message);
 
int main(int argc, char* argv[])
{
    WSADATA wsaData;
    SOCKET hSocket;
    char message[BUF_SIZE];
    int strLen;
    SOCKADDR_IN servAddr;
 
    if (argc != 3) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }
 
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");
 
    hSocket = socket(PF_INET, SOCK_STREAM, 0);
    if (hSocket == INVALID_SOCKET)
        ErrorHandling("socket() error!");
 
    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(argv[1]);
    servAddr.sin_port = htons(atoi(argv[2]));
 
    if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
        ErrorHandling("connect() error!");
    else
        puts("Connected..........");
 
    while (1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
 
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;
 
        send(hSocket, message, strlen(message), 0);
        strLen = recv(hSocket, message, BUF_SIZE - 1, 0);
        message[strLen] = 0;
        printf("Message from server: %s", message);
    }
 
    closesocket(hSocket);
    WSACleanup();
    return 0;
}
 
void ErrorHandling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

1. client에서는 socket을 생성한 후, server로 connect() 함수를 이용하여 연결 시도

2. 이 때 connect() 함수를 이용하여 연결을 시도했다고 해서 server에서 accept() 함수를 통해 연결을 수락했다는 의미는 아님

3. connect() 함수 호출이 정상적으로 반환되었다는 것은 server에서 listen() 함수 호출 후에 생성된 연결 대기 queue에 접수된 것으로 이해해야 함

 

Windbg를 통한 TCP State 확인

1. 먼저 작성한 Server 프로그램을 인자값 9000(포트 번호)과 함께 Windbg를 이용하여 Open 합니다.

 

2. Symbol Path를 설정해야 하는데, Server 프로그램의 Symbol 파일과 Source 파일은 테스트를 위해 C:\Temp 에 위치시켜 놓은 상태입니다.

0:000> .sympath c:\temp;srv*c:\sym\websym*https://msdl.microsoft.com/download/symbols
Symbol search path is: c:\temp;srv*c:\sym\websym*https://msdl.microsoft.com/download/symbols
Expanded Symbol search path is: c:\temp;srv*c:\sym\websym*https://msdl.microsoft.com/download/symbols
 
************* Path validation summary **************
Response                         Time (ms)     Location
OK                                             c:\temp
Deferred                                       srv*c:\sym\websym*https://msdl.microsoft.com/download/symbols

 

3. Source Code에서 Breakpoint 설정을 위해 Source 파일을 Open 합니다.

 

4. 새로 생성되는 TCP 구조체의 State를 확인하기 위해 아래 함수의 위치마다 Breakpoint를 설정합니다.

※ socket(), bind(), listen(), accept()

 

5. 함수 하나씩 실행 순서를 지나가면서 netstat 결과를 확인합니다.

※ 참고로 Windows에서 netstat 실행 시 옵션 중 "q"는 listening 상태의 port 뿐만 아니라 bind 상태의 port도 보여줍니다.

5-1. socket() 호출 전

0:000> g
Breakpoint 0 hit
server!main+0xc3:
00007ff7`973f5c93 4533c0          xor     r8d,r8d
 
C:\>netstat -anoq | findstr 9000

 

5-2. socket() 호출 후 / bind() 호출 전

0:000> g
ModLoad: 00007ff9`071f0000 00007ff9`07257000   C:\Windows\system32\mswsock.dll
Breakpoint 1 hit
server!main+0x14a:
00007ff7`973f5d1a 41b810000000    mov     r8d,10h
 
C:\>netstat -anoq | findstr 9000

 

5-3. bind() 호출 후 / listen() 호출 전

※ 9000 포트로 BOUND 상태가 확인됩니다.

0:000> g
Breakpoint 2 hit
server!main+0x175:
00007ff7`973f5d45 ba05000000      mov     edx,5
 
C:\>netstat -anoq | findstr 9000
  TCP    0.0.0.0:9000           0.0.0.0:0              BOUND           94952

 

5-4. listen() 호출 후 / accept() 호출 전

※ 9000 포트로 LISTENING 상태가 확인됩니다.

0:000> g
Breakpoint 3 hit
server!main+0x1c9:
00007ff7`973f5d99 4c8d85c4060000  lea     r8,[rbp+6C4h]
 
C:\>netstat -anoq | findstr 9000
  TCP    0.0.0.0:9000           0.0.0.0:0              LISTENING       94952