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

目录

一,函数清单

1.socket 方法

2.bind 方法

3.listen 方法

4.accept 方法(阻塞函数)

5.recv 方法(阻塞函数)

6.send 方法

7.close 方法

8.htonl 方法

9.htons 方法

扫描二维码关注公众号,回复: 15602206 查看本文章

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