【高并发网络通信架构】1.Linux下实现单客户连接的tcp服务端

目录

一,函数清单

1.socket 方法

2.bind 方法

3.listen 方法

4.accept 方法(阻塞函数)

5.recv 方法(阻塞函数)

6.send 方法

7.close 方法

8.htonl 方法

9.htons 方法

10.fcntl 方法

二,代码实现

1.阻塞型服务端

TCP服务端程序的一般流程

TCP客户端程序的一般流程

完整代码

2.非阻塞型服务端

非阻塞型TCP服务端的一般流程

完整代码


一,函数清单

1.socket 方法

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

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

功能

  • 创建用于通信的套接字,并返回一个指向该套接字的文件描述符。

参数

  1. domain:指定套接字的协议族。常见的值有AF_INET(IPv4)和AF_INET6(IPv6)。
  2. type:指定套接字的类型。常见的值有SOCK_STREAM(面向连接的可靠字节流)和SOCK_DGRAM(无连接的数据报文)。
  3. protocol:指定协议。通常使用0,表示默认选择。

返回值

  • 如果成功,则返回新套接字的文件描述符。如果出现错误,则返回-1,并设置errno。

2.bind 方法

#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.listen 方法

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

int listen(int sockfd, int backlog);

功能

  • 开始监听指定套接字上的连接请求。

参数

  1. sockfd:socket返回的套接字描述符。
  2. backlog:等待连接队列的最大长度。如果连接请求到达时如果队列已满,则客户端可能会收到ECONNREFUSED指示的错误,如果底层协议支持重传,则请求可能已满忽略,以便稍后重试连接成功。

返回值

  • 如果成功,则返回0。如果出现错误,则返回-1,并设置errno。

4.accept 方法(阻塞函数)

#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:accept返回的套接字描述符。
  2. buf:接收数据的缓冲区。
  3. len:缓冲区的长度。
  4. flags:接收操作的标志,一般设置为0。

返回值

  • 返回接收到的字节数,如果发生错误则返回-1。如果发生错误,则设置errno来指示错误。当客户端连接关闭时,返回值将为0。

6.send 方法

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

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

功能

  • 向已连接的套接字发送数据。

参数

  1. sockfd:accept返回的套接字描述符。
  2. buf:包含要发送数据的缓冲区。
  3. len:要发送的数据长度。
  4. flags:发送操作的标志,一般设置为0。

返回值

  • 如果成功,这些调用将返回发送的字节数。如果出现错误,则返回-1,并设置errno。

7.close 方法

#include <unistd.h>

int close(int fd);

功能

  • 关闭文件描述符,释放相关资源

参数

  1. fd:要关闭的文件描述符。

返回值

  • 成功返回零。如果出现错误,则返回-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):使用socket系统调用创建一个TCP套接字。套接字是网络通信的端点。

  2. 绑定地址和端口(Bind):将服务器的IP地址和端口号与套接字绑定,使用bind系统调用来完成绑定操作。

  3. 监听连接请求(Listen):将套接字置于监听状态,等待客户端的连接请求。使用listen系统调用设置套接字的监听队列长度。

  4. 接受连接请求(Accept):当有客户端请求连接时,使用accept系统调用接受连接请求。这将创建一个新的套接字,用于和客户端进行通信,而原始的监听套接字继续监听新的连接请求。

  5. 进行通信(Communicate):使用接受到的套接字进行数据通信。可以使用read/recvwrite/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):使用socket系统调用创建一个TCP套接字。

  2. 设置服务器地址和端口号:使用struct sockaddr_in结构体来表示服务器的地址和端口号。根据服务器的IP地址和端口号来填充该结构体。

  3. 连接服务器(Connect):使用connect系统调用将套接字连接到服务器。将服务器的地址和端口号作为参数传递给connect函数。

  4. 进行数据通信(Communicate):使用已连接的套接字进行数据的读取和写入。可以使用readwrite系统调用读取和发送数据。

  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放在一个master循环里,用于一直读取客户端发来的数据。
#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.非阻塞型服务端

非阻塞型TCP服务端的一般流程

  1. 创建套接字(Socket):使用socket系统调用创建一个TCP套接字。

  2. 设置套接字为非阻塞模式:使用fcntl函数,通过F_SETFL命令将套接字的文件状态标志设置为非阻塞模式,即使用O_NONBLOCK标志。

  3. 绑定地址和端口(Bind):将服务器的IP地址和端口号与套接字绑定,使用bind系统调用来完成绑定操作。

  4. 监听连接请求(Listen):将套接字置于监听状态,等待客户端的连接请求,使用listen系统调用设置套接字的监听队列长度。

  5. 接受连接请求(Accept):使用accept系统调用接受连接请求。这将创建一个新的套接字,用于和客户端进行通信,而原始的监听套接字继续监听新的连接请求。

  6. 设置新的套接字为非阻塞模式:同样,使用fcntl函数设置新的套接字为非阻塞模式。

  7. 进行数据通信(Communicate):使用非阻塞的套接字进行数据的读取和写入。可以使用read/recvwrite/send系统调用或其他非阻塞的I/O函数进行数据交换。

  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;
}

运行效果

  • 编译成功并运行后,报了如下错误,导致服务端不能正常运行。

问题分析

  • 错误码"Resource temporarily unavailable"(资源暂时不可用)在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