[높은 동시 네트워크 통신 아키텍처] 1. Linux에서 단일 클라이언트 연결을 위한 tcp 서버

목차

1. 기능 목록

1. 소켓 방식

2. 바인드 방법

3. 듣기 방법

4. 수락 방식(블로킹 기능)

5. recv 방식(블로킹 기능)

6. 보내기 방법

7. 닫기 방법

8. htonl 방법

9. htons 방법

10. fcntl 방법

둘째, 코드 구현

1. 차단 서버

TCP 서버 프로그램의 일반적인 흐름

TCP 클라이언트 프로그램의 일반적인 흐름

전체 코드

2. 논블로킹 서버

Non-blocking TCP 서버의 일반적인 흐름

전체 코드


1. 기능 목록

1. 소켓 방식

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

기능

  • 통신용 소켓을 만들고 해당 소켓을 가리키는 파일 설명자를 반환합니다.

매개변수

  1. 도메인: 소켓의 프로토콜 제품군을 지정합니다. 일반적인 값은 AF_INET(IPv4) 및 AF_INET6(IPv6)입니다.
  2. 유형: 소켓 유형을 지정합니다. 일반적인 값은 SOCK_STREAM(연결 지향 신뢰할 수 있는 바이트 스트림) 및 SOCK_DGRAM(연결 없는 데이터그램)입니다.
  3. 프로토콜: 프로토콜을 지정합니다. 일반적으로 0이 사용되며 이는 기본 선택을 의미합니다.

반환 값

  • 성공하면 새 소켓에 대한 파일 설명자를 반환합니다. 오류가 발생하면 -1이 반환되고 errno가 설정됩니다.

2. 바인드 방법

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

기능

  • 소켓을 특정 IP 주소 및 포트에 바인딩합니다.

매개변수

  1. sockfd: socket이 반환한 소켓 설명자.
  2. addr: 바인드할 로컬 주소를 가리키는 구조(보통 sockaddr_in 또는 sockaddr_in6 구조).
  3. addrlen: 로컬 주소의 길이(일반적으로 sizeof(struct sockaddr_in) 또는 sizeof(struct sockaddr_in6)).

반환 값

  • 성공하면 0을 반환합니다. 오류가 발생하면 -1이 반환되고 errno가 설정됩니다.

3. 듣기 방법

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

기능

  • 지정된 소켓에서 연결 요청 수신 대기를 시작합니다.

매개변수

  1. sockfd: socket이 반환한 소켓 설명자.
  2. 백로그: 연결 대기 큐의 최대 길이입니다. 연결 요청이 도착했을 때 대기열이 가득 찬 경우 클라이언트는 ECONNREFUSED로 표시된 오류를 수신할 수 있으며 기본 프로토콜이 재전송을 지원하는 경우 연결이 성공하면 나중에 연결을 다시 시도할 수 있도록 요청을 무시할 수 있습니다.

반환 값

  • 성공하면 0을 반환합니다. 오류가 발생하면 -1이 반환되고 errno가 설정됩니다.

4. 수락 방식(블로킹 기능)

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

기능

  • 연결 요청을 수락하고 클라이언트와 통신하기 위해 새 소켓 설명자를 반환합니다.

매개변수

  1. sockfd: socket이 반환한 소켓 설명자.
  2. addr: 클라이언트 주소를 저장하는 데 사용되는 구조에 대한 포인터. 일반적으로 struct sockaddr_in 구조로 지정됩니다.
  3. addrlen: addr 구조의 길이를 전달하는 데 사용됩니다.

반환 값

  • 성공하면 이러한 시스템 호출은 수락된 소켓의 파일 설명자(음이 아닌 정수)를 반환합니다. 오류가 발생하면 -1을 반환하고 errno를 적절하게 설정하고 addrlen을 변경하지 않고 유지합니다.

5. recv 방식(블로킹 기능)

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

기능

  • 연결된 소켓에서 데이터를 받습니다.

매개변수

  1. sockfd: 수락에 의해 반환된 소켓 설명자.
  2. buf: 데이터를 받기 위한 버퍼.
  3. len: 버퍼의 길이.
  4. flags: 수신 작업을 위한 플래그로 일반적으로 0으로 설정됩니다.

반환 값

  • 받은 바이트 수를 반환하거나 오류가 발생한 경우 -1을 반환합니다. 오류가 발생하면 오류를 나타내도록 errno가 설정됩니다. 클라이언트 연결이 닫히면 반환 값은 0이 됩니다.

6. 보내기 방법

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

기능

  • 연결된 소켓에 데이터를 보냅니다.

매개변수

  1. sockfd: 수락에 의해 반환된 소켓 설명자.
  2. buf: 보낼 데이터를 포함하는 버퍼.
  3. len: 보낼 데이터의 길이.
  4. flags: 보내는 작업의 플래그이며 일반적으로 0으로 설정됩니다.

반환 값

  • 성공하면 이러한 호출은 전송된 바이트 수를 반환합니다. 오류가 발생하면 -1이 반환되고 errno가 설정됩니다.

7. 닫기 방법

#include <unistd.h>

int close(int fd);

기능

  • 파일 설명자를 닫고 관련 리소스를 해제합니다.

매개변수

  1. fd: 닫을 파일 설명자.

반환 값

  • 성공 시 0을 반환합니다. 오류가 발생하면 -1이 반환되고 errno가 설정됩니다.

8. htonl 방법

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);

기능

  • 호스트 바이트 순서의 32비트(4바이트) 부호 없는 정수를 네트워크 바이트 순서의 정수로 변환합니다.

9. htons 방법

#include <arpa/inet.h>

uint16_t htons(uint16_t hostshort);

기능

  • 호스트 바이트 순서의 16비트(2바이트) 부호 있는 짧은 정수를 네트워크 바이트 순서의 정수로 변환합니다.

10. fcntl 방법

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

기능

  • 작동 파일 디스크립터의 동작 및 속성은 비차단 IO로 설정할 수 있습니다.

매개변수

  1. fd: 설정할 파일 설명자.
  2. cmd: fd에서 작업을 수행하는 명령으로, 일반적인 명령은 다음과 같으며 일반적으로 F_GETFL, F_SETFL을 사용합니다.
    1. F_DUPFD: 파일 설명자를 복사합니다.
    2. F_GETFD: 파일 설명자 플래그를 가져옵니다.
    3. F_SETFD: 파일 설명자 플래그를 설정합니다.
    4. F_GETFL: 파일 상태 플래그를 가져옵니다.
    5. F_SETFL: 파일 상태 플래그를 설정합니다.
    6. F_GETLK: 파일 잠금을 획득합니다.
    7. F_SETLK: 파일 잠금을 설정합니다.
    8. F_SETLKW: 파일 잠금을 설정하고 잠금을 사용할 수 없는 경우 대기합니다.

반환 값

  • 성공적인 호출을 위해서는 동작 명령에 따라 반환 값이 달라지며 에러가 발생하면 -1이 반환되고 errno가 적절하게 설정됩니다.

둘째, 코드 구현

1. 차단 서버

TCP 서버 프로그램의 일반적인 흐름

  1. 소켓 만들기: socket시스템 호출을 사용하여 TCP 소켓을 만듭니다. 소켓은 네트워크 통신을 위한 끝점입니다.

  2. 주소 및 포트 바인딩(Bind): 서버의 IP 주소와 포트 번호를 소켓에 바인딩하고 bind시스템 호출을 사용하여 바인딩 작업을 완료합니다.

  3. 청취 연결 요청(Listen): 소켓을 청취 상태로 두고 클라이언트의 연결 요청을 기다립니다. 시스템 호출을 사용하여 listen소켓의 청취 대기열 길이를 설정하십시오.

  4. 연결 요청 수락(Accept): 클라이언트가 연결을 요청하면 accept시스템 호출을 사용하여 연결 요청을 수락합니다. 이렇게 하면 클라이언트와 통신하기 위한 새 소켓이 생성되고 원래 수신 소켓은 계속해서 새 연결 요청을 수신합니다.

  5. 통신: 데이터 통신을 위해 받은 소켓을 사용합니다. read/recv시스템 write/send호출 또는 기타 고급 I/O 기능을 사용하여 데이터를 송수신할 수 있습니다 .

  6. 소켓 닫기(Close): 통신이 끝나면 close시스템 콜을 이용해 소켓을 닫고 관련 자원을 해제한다.

샘플 코드

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    // 创建套接字
    int serverSocket = socket(AF_INET, SOCK_STREAM, 0);

    // 绑定地址和端口
    struct sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;    //ipv4
    serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddress.sin_port = htons(8888);
    bind(serverSocket, (struct sockaddr *)&serverAddress, sizeof(serverAddress));

    // 监听连接请求
    listen(serverSocket, 5);

    // 接受连接请求
    int clientSocket = accept(serverSocket, NULL, NULL);

    // 进行通信
    char buffer[1024];
    read(clientSocket, buffer, sizeof(buffer));
    printf("Received message: %s\n", buffer);

    // 关闭套接字
    close(clientSocket);
    close(serverSocket);

    return 0;
}

TCP 클라이언트 프로그램의 일반적인 흐름

  1. 소켓 만들기: socket시스템 호출을 사용하여 TCP 소켓을 만듭니다.

  2. 서버 주소 및 포트 번호 설정: struct sockaddr_in구조를 사용하여 서버의 주소 및 포트 번호를 나타냅니다. 이 구조를 서버의 IP 주소와 포트 번호로 채웁니다.

  3. 서버에 연결(Connect): connect시스템 호출을 사용하여 소켓을 서버에 연결합니다. 서버의 주소와 포트 번호를 매개변수로 connect함수에 전달합니다.

  4. 데이터 통신(통신): 연결된 소켓을 사용하여 데이터를 읽고 씁니다. 데이터는 read및 시스템 호출을 사용하여 읽고 보낼 수 있습니다 .write

  5. 소켓 닫기(Close): 통신이 완료되면 close시스템 콜을 이용해 소켓을 닫고 관련 자원을 해제한다.

샘플 코드

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    // 创建套接字
    int clientSocket = socket(AF_INET, SOCK_STREAM, 0);

    // 设置服务器地址和端口号
    struct sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_port = htons(8888);
    serverAddress.sin_addr.s_addr = inet_addr("服务器IP地址");

    // 连接服务器
    connect(clientSocket, (struct sockaddr *)&serverAddress, sizeof(serverAddress));

    // 进行数据通信
    char *message = "Hello, server!";
    send(clientSocket, message, strlen(message),0);

    // 关闭套接字
    close(clientSocket);

    return 0;
}

전체 코드

  • accept와 recv는 모두 차단 기능으로, accept에서는 클라이언트의 연결이 차단되고 recv에서는 연결된 클라이언트의 데이터를 읽을 수 없도록 차단됩니다.
  • 클라이언트와의 지속적인 통신을 위해서는 recv를 마스터 루프에 배치하여 항상 클라이언트가 보낸 데이터를 읽어야 합니다.
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>


#define BUFFER_LENGTH   1024

//初始化服务端,返回其文件描述符
int init_server(int port){
    //返回服务端fd,通常为3,前面0,1,2用于表示标准输入,输出,错误值
    int sfd = socket(AF_INET,SOCK_STREAM,0);

    if(-1 == sfd){
        printf("socket error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    struct sockaddr_in servAddr;
    memset(&servAddr,0,sizeof(struct sockaddr_in));
    servAddr.sin_family = AF_INET;  //ipv4
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);   //0.0.0.0
    servAddr.sin_port = htons(port);    //端口号
    
    //绑定IP和端口号
    if(-1 == bind(sfd,(struct sockaddr*)&servAddr,sizeof(struct sockaddr_in)))
    {
        printf("bind error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    //监听该套接字上的连接
    if(-1 == listen(sfd,SOMAXCONN))
    {
        printf("listen error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    return sfd;
}

int main(int argc,char *argv[]){

    if(argc < 2)return -1;

    int port = atoi(argv[1]);   //atoi:将字符串转换为整数
    int sfd = init_server(port);
    printf("server fd: %d\n",sfd);

    struct sockaddr_in clientAddr;
    socklen_t len = sizeof(struct sockaddr_in);
    int cfd = accept(sfd,(struct sockaddr*)&clientAddr,&len);   //阻塞函数
    printf("client fd: %d\n",cfd);
    while (1)
    {
        char data[BUFFER_LENGTH]={0};
        int recvLen = recv(cfd,data,BUFFER_LENGTH,0);    //阻塞函数
        if(recvLen < 0){
            printf("recv client fd %d errno: %d\n",cfd,errno);
        }else if(recvLen == 0){
            printf("client fd %d close\n",cfd);
            close(cfd);     //关闭客户端文件描述符,释放资源
            break;
        }else{
            printf("recv client fd %d data: %s\n",cfd,data);
            send(cfd,data,recvLen,0);
        }
    }

    close(sfd);     //关闭服务端文件描述符,释放资源
    printf("server fd %d close\n",sfd);
    
    return 0;
}

실행 결과

  • 테스트 도구: NetAssist는  클라이언트 도구를 시뮬레이트하여 서버 코드를 테스트합니다.

2. 논블로킹 서버

Non-blocking TCP 서버의 일반적인 흐름

  1. 소켓 만들기: socket시스템 호출을 사용하여 TCP 소켓을 만듭니다.

  2. 소켓을 비차단 모드로 설정: fcntl함수를 사용 F_SETFL하여 소켓의 파일 상태 플래그를 명령을 통해 비차단 모드로 설정합니다. 즉, O_NONBLOCK플래그를 사용합니다.

  3. 주소 및 포트 바인딩(Bind): 서버의 IP 주소와 포트 번호를 소켓에 바인딩하고 bind시스템 호출을 사용하여 바인딩 작업을 완료합니다.

  4. 청취 연결 요청(Listen): 소켓을 청취 상태에 놓고 클라이언트의 연결 요청을 기다린 후 시스템 listen호출을 사용하여 소켓의 청취 대기열 길이를 설정합니다.

  5. 연결 요청 수락(Accept): accept시스템 호출을 사용하여 연결 요청을 수락합니다. 이렇게 하면 클라이언트와 통신하기 위한 새 소켓이 생성되고 원래 수신 소켓은 계속해서 새 연결 요청을 수신합니다.

  6. 새 소켓을 비차단 모드로 설정: 마찬가지로 fcntl함수를 사용하여 새 소켓을 비차단 모드로 설정합니다.

  7. 데이터 통신(통신): 비차단 소켓을 사용하여 데이터를 읽고 씁니다. read/recv데이터는 시스템 호출 또는 기타 비차단 I/O 기능 과 write/send교환될 수 있습니다 .

  8. 소켓 닫기(Close): 통신이 끝나면 close시스템 콜을 이용해 소켓을 닫고 관련 자원을 해제한다.

샘플 코드

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>

int main() {
    // 创建套接字
    int serverSocket = socket(AF_INET, SOCK_STREAM, 0);

    // 设置套接字为非阻塞模式
    int flags = fcntl(serverSocket, F_GETFL, 0);
    fcntl(serverSocket, F_SETFL, flags | O_NONBLOCK);

    // 绑定地址和端口
    struct sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddress.sin_port = htons(8888);
    bind(serverSocket, (struct sockaddr *)&serverAddress, sizeof(serverAddress));

    // 监听连接请求
    listen(serverSocket, 5);

    while (1) {
        // 接受连接请求
        struct sockaddr_in clientAddress;
        socklen_t clientAddressLength = sizeof(clientAddress);
        int clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddress, &clientAddressLength);

        if (clientSocket > 0) {
            // 设置新的套接字为非阻塞模式
            flags = fcntl(clientSocket, F_GETFL, 0);
            fcntl(clientSocket, F_SETFL, flags | O_NONBLOCK);

            // 进行数据通信
            char buffer[1024];
            ssize_t bytesRead = read(clientSocket, buffer, sizeof(buffer));
            if (bytesRead > 0) {
                // 读取到数据
                printf("Received message from client: %s\n", buffer);
            }

            // 关闭客户端套接字
            close(clientSocket);
        }
    }

    // 关闭服务端套接字
    close(serverSocket);

    return 0;
}

전체 코드

  • 소켓을 비차단 모드로 설정합니다.
  • 다음은 위의 코드 조각이 서버를 비차단으로 설정하는지 여부를 테스트하는 데 사용됩니다.
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <fcntl.h>


#define BUFFER_LENGTH   1024

int init_server(int port){
    //获取服务端fd,通常为3,前面0,1,2用于指定输入,输出,错误值
    int sfd = socket(AF_INET,SOCK_STREAM,0);

    if(-1 == sfd){
        printf("socket error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    //设置服务端套接字为非阻塞模式
    int flags = fcntl(sfd,F_GETFL,0);
    fcntl(sfd,F_SETFL,flags | O_NONBLOCK);

    struct sockaddr_in servAddr;
    memset(&servAddr,0,sizeof(struct sockaddr_in));
    servAddr.sin_family = AF_INET;  //ipv4
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);   //0.0.0.0
    servAddr.sin_port = htons(port);
    //服务端绑定ip地址和端口号
    if(-1 == bind(sfd,(struct sockaddr*)&servAddr,sizeof(struct sockaddr_in)))
    {
        printf("bind error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    //监听该套接字上的连接请求
    if(-1 == listen(sfd,SOMAXCONN))
    {
        printf("listen error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }

    return sfd;
}

int main(int argc,char *argv[]){

    if(argc < 2)return -1;

    int port = atoi(argv[1]);
    int sfd = init_server(port);
    printf("server fd: %d\n",sfd);

    //接受连接请求
    struct sockaddr_in clientAddr;
    socklen_t len = sizeof(struct sockaddr_in);
    int cfd = accept(sfd,(struct sockaddr*)&clientAddr,&len);
    if(cfd == -1){
        printf("accept error code: %d codeInfo: %s\n",errno,strerror(errno));
        return -1;
    }
    printf("client fd: %d\n",cfd);
    //设置新的套接字为非阻塞模式
    int flags = fcntl(cfd,F_GETFL,0);
    fcntl(cfd,F_SETFL,flags | O_NONBLOCK);

    while (1)
    {
        char data[BUFFER_LENGTH]={0};
        int recvLen = recv(cfd,data,BUFFER_LENGTH,0);
        if(recvLen < 0){
            printf("recv client fd %d errno: %d\n",cfd,errno);
        }else if(recvLen == 0){
            //客户端断开连接
            printf("client fd %d close\n",cfd);
            close(cfd);     //关闭客户端文件描述符,释放资源
            break;
        }else{
            printf("recv client fd %d data: %s\n",cfd,data);
            send(cfd,data,recvLen,0);
        }
    }

    close(sfd);     //关闭服务端文件描述符,释放资源
    printf("server fd %d close\n",sfd);
    
    return 0;
}

실행 효과

  • 성공적으로 컴파일 및 실행한 후 다음 오류가 보고되어 서버가 정상적으로 실행되지 않습니다.

문제 분석

  • Linux에서 "리소스를 일시적으로 사용할 수 없음"(리소스를 일시적으로 사용할 수 없음)의 해당 오류 코드는 EAGAIN(오류 값 11) 또는 EWOULDBLOCK입니다. 이 오류 코드는 일반적으로 비차단 I/O 작업 중에 발생하며 현재 사용 가능한 리소스가 없거나 작업이 진행 중임을 나타냅니다.
  • 네트워크 프로그래밍에서 읽기 또는 쓰기 작업을 위해 비차단 모드의 소켓을 사용할 때 사용 가능한 데이터가 없거나 쓰기 작업을 즉시 완료할 수 없는 경우 이 오류 코드가 반환될 수 있습니다. 이는 논블로킹 모드의 I/O 작업이 논블로킹이기 때문입니다. 즉, 즉시 완료되거나 기다리지 않고 오류 코드를 반환합니다.

해결책

  • 비동기 I/O(Asynchronous I/O): 비동기 I/O 작업을 사용하면 현재 스레드를 차단하지 않고 모든 I/O 작업 후에 반환할 수 있습니다. Linux는 aio_read 및 aio_write와 같은 비동기 I/O 기능을 제공합니다. 비동기 I/O 작업을 사용하면 작업이 완료될 때 알림을 받을 콜백 함수를 등록할 수 있습니다.

다루기 위한 후속 연구. . .

추천

출처blog.csdn.net/weixin_43729127/article/details/131578334