Linux下的socket操作

一、TCP服务端

创建一个TCP服务器的基本操作:

  1. 创建一个套接字(socket):使用socket函数
  2. 绑定套接字(socket):将套接字绑定到一个特定的IP地址和端口号上,这些信息要用结构体sockaddr_in来保存
  3. 监听请求连接:使用listen函数
  4. 接受连接:使用accept函数来实现
  5. 发送和接受信息:一旦建立了连接,服务器和客户端都可以使用套接字的send()和recv()方法来发送和接收数据。发送方使用send()方法将数据发送到套接字,而接收方使用recv()方法从套接字接收数据。
  6. 关闭连接:当通信完成后,可以调用套接字的close()方法来关闭连接。这将释放套接字占用的资源并终止连接。

1.socket:创建套接字

函数原型:

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

参数说明:

  • domain:指定地址族,可以是AF_INET(用于IPv4)或AF_INET6(用于IPv6)。
  • type:指定套接字类型,可以是SOCK_STREAM(用于TCP)或SOCK_DGRAM(用于UDP)。
  • protocol:一般为0,表示使用默认的协议。

返回值: 如果成功,返回一个非负整数,表示套接字文件描述符(socket file descriptor)。如果失败,返回-1,并设置全局变量errno来指示错误类型。

2. sockaddr_in:sockaddr_in 是一个用于表示 IPv4 地址的结构体,它定义在 <netinet/in.h> 头文件中。

定义如下:

struct sockaddr_in {
    sa_family_t sin_family; // 地址族,一般为 AF_INET
    in_port_t sin_port;     // 端口号
    struct in_addr sin_addr; // IPv4 地址
    char sin_zero[8];       // 用于补齐,一般设置为全0
};

其中,sa_family_t 和 in_port_t 是整数类型,struct in_addr 是一个用于存储 IPv4 地址的结构体。sin_family 表示地址族,一般为 AF_INET,表示使用 IPv4 地址。sin_port 表示端口号,用于标识网络中的应用程序。sin_addr 存储了 IPv4 地址的信息。sin_zero 是一个用于补齐的字段,一般设置为全0。

in_addr:in_addr 是一个用于存储 IPv4 地址的结构体,它定义在 <netinet/in.h> 头文件中。

它的定义如下:

struct in_addr {
    in_addr_t s_addr; // IPv4 地址
};

其中,in_addr_t 是一个无符号整数类型,用于存储 IPv4 地址。

3. htons:htons 是一个用于将主机字节序(Host Byte Order)转换为网络字节序(即大端存储)的函数。它定义在 <arpa/inet.h> 头文件中,并且接受一个参数,表示要转换的 16 位无符号整数。

函数原型如下:

uint16_t htons(uint16_t hostshort);

4.bind:bind 是一个用于将一个套接字(socket)与一个特定的地址(包括 IP 地址和端口号)绑定的函数。它通常用于服务器端在监听连接之前将套接字绑定到一个特定的地址上。

在 C 语言中,bind 函数的原型如下:

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

参数说明:

  • sockfd 是要绑定的套接字的文件描述符。
  • addr 是一个指向 sockaddr 结构体的指针,用于指定要绑定的地址信息。
  • addrlen 是 addr 所指向的结构体的长度。

返回值:

  • 如果 bind 函数执行成功,返回值为 0。这意味着套接字成功地与指定的地址绑定在一起。
  • 如果 bind 函数执行失败,返回值为 -1。这表示绑定操作未成功。

5.listen:listen 函数用于将一个已绑定的套接字(socket)设置为监听状态,以便接受传入的连接请求

在 C 语言中,listen 函数的原型如下:

int listen(int sockfd, int backlog);

参数说明:

  • sockfd:要设置为监听状态的套接字的文件描述符。
  • backlog:定义在连接队列中等待被接受的连接的最大数量。

返回值:

  • 如果 listen 函数执行成功,返回值为 0。这意味着套接字已成功设置为监听状态,并且可以开始接受传入的连接请求。
  • 如果 listen 函数执行失败,返回值为 -1。这表示设置监听状态的操作未成功。

6.accept:accept 函数用于从已监听的套接字中接受一个传入的连接请求,并创建一个新的套接字来处理该连接。

在 C 语言中,accept 函数的原型如下:

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

参数说明:

  • sockfd:已监听的套接字的文件描述符。
  • addr:指向一个 struct sockaddr 结构的指针,用于存储接受连接的远程地址信息。
  • addrlen:指向一个 socklen_t 类型的变量,表示 addr 的大小。

返回值:

  • 如果 accept 函数执行成功,返回值为新创建的套接字的文件描述符。这个新套接字用于与客户端进行通信。
  • 如果 accept 函数执行失败,返回值为 -1。这表示接受连接请求的操作未成功。

7.recv:recv 函数用于从已连接的套接字接收数据。它是在已建立连接的套接字上进行数据交换的常用函数之一。

在 C 语言中,recv 函数的原型如下:

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

参数含义:

  • sockfd:已连接的套接字的文件描述符。
  • buf:指向接收数据的缓冲区的指针。
  • len:缓冲区的大小,即要接收的最大字节数。
  • flags:可选的标志参数,用于控制接收操作的行为。

返回值: recv 函数会阻塞程序执行,直到有数据到达为止。当有数据到达时,recv 函数会将数据从套接字读取到指定的缓冲区 buf 中,并返回实际接收到的字节数。如果返回值为 0,表示对端已关闭连接。如果返回值为 -1,表示接收数据的操作未成功。

8.创建服务器实例

#include <stdio.h>                                                                                                                                  
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    //创建socket,参数说明:
    //指定协议族:AF_INET(IPv4)和 AF_INET6(IPv6)
    //指定套接字使用的协议,通常为0,表示根据 domain 和 type 自动选择合适的协议。
    int sockfd = socket(AF_INET, SOCK_STREAM, 0); 
    if(-1 == sockfd)
    {   
        perror("socket");
        exit(1);
    }   

    //绑定
    //sockaddr_in 是一个用于表示 IPv4 地址的结构体
    struct sockaddr_in server_info; //用于保存服务器的信息(IP PROT)
    bzero(&server_info, sizeof(struct sockaddr_in)); //清空,bzero() 是一个函数,用于将指定内存区域的前几个字节设置为零
    server_info.sin_family = AF_INET; //地址族
    server_info.sin_port = htons(7000); //端口,大于1024都行
//  server_info.sin_addr.s_addr = inet_addr("127.0.0.1"); //表示回环IP,用于测试
    server_info.sin_addr.s_addr = inet_addr("192.168.175.129");

    if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1) 
    {   
        perror("bind");
   exit(2);
    }

    //设置监听队列
    if(listen(sockfd, 10) == -1)
    {
        perror("listen");
        exit(3);
    }
    
    printf("等待客户端的连接...\n");
    
    //接受连接 
    struct sockaddr_in client_info;
    int length = sizeof(client_info);
    int fd = accept(sockfd, (struct sockaddr *)&client_info, &length);
    if(-1 == fd)
    {
        perror("accept");
        exit(4);
    }
    
    printf("接受客户端的连接 %d\n", fd);
    
    char buf[1024] = {0};
    ssize_t size;
    
    while(1)
    {
        size = recv(fd, buf, sizeof(buf), 0);
        if(size == -1)
        {
            perror("recv");
            break;
        }
    
        else if(size == 0)
            break;
        if(!strcmp(buf, "bye"))
            break;
    
        printf("%s\n", buf);
        bzero(buf, 1024);
    }
    
    close(fd); //关闭TCP连接,不能在接受数据
    close(sockfd); //关闭socket,不能再处理客户端的请求
    
    //socket用于处理客户端的连接,fd用于处理客户端的消息
    return 0;
}       

二、TCP的客户端

客户端连接服务端的基本操作:

  1. 创建socket
  2. 发起连接
  3. 发送信息

1.send:send 是一个函数,用于在一个已连接的套接字上发送数据。

函数原型如下:

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

参数说明:

  • sockfd:已连接的套接字描述符。
  • buf:指向要发送数据的缓冲区的指针。
  • len:要发送的数据的长度(以字节为单位)。
  • flags:可选的标志参数,用于控制发送操作的行为。

返回值: 函数返回一个 ssize_t 类型的值,表示实际发送的字节数。如果发送失败,返回值为 -1,并且可以通过检查全局变量 errno 来获取错误代码。

2.connect

connect 函数用于在客户端程序中建立与远程主机的连接。
函数原型:

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

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

参数:

  • sockfd:已经创建好的套接字描述符,用于标识客户端的套接字。
  • addr:指向 struct sockaddr 类型的指针,其中包含了要连接的远程主机的地址信息。
  • addrlen:addr 结构体的大小。

返回值:

  • 成功连接时,返回值为 0。
  • 连接失败时,返回值为 -1,并设置相应的错误码,可以通过 errno 全局变量获取具体的错误信息。

3.创建客户端实例

#include <stdio.h>                                                                                                                                  
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    //创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {
        perror("socket");
        exit(1);
    }

    //发起连接
    struct sockaddr_in server_info; // 存储服务器信息
    bzero(&server_info, 0); 
    server_info.sin_family = AF_INET;  //地址族
    server_info.sin_port = htons(7000); //绑定端口
    server_info.sin_addr.s_addr = inet_addr("192.168.175.129"); //绑定ip地址
    if(connect(sockfd, (struct sockaddr*)&server_info, sizeof(server_info)) == -1)
    {
        perror("connect");
        exit(2);
    }

    //发送信息
    char buf[1024] = {0};
     while(1)
    {
        scanf("%s", buf);
        if(send(sockfd, buf, sizeof(buf), 0) == -1)
        {
            perror("send");
            exit(3);
        }

        if(!strcmp(buf, "bye"))
            break;
        
        bzero(buf, sizeof(buf));

    }
    
    close(sockfd);
    return 0;
}   

三、TCP并发服务器

使用多线程来实现响应多个客户端

#include <stdio.h>                                                                                                                                  
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

void *client_recv(void *arg)
{
    int fd = *(int *)arg;

    char buf[1024] = {0};
    ssize_t size;

    while(1)
    {   
        size = recv(fd, buf, sizeof(buf), 0); 
        if(size == -1) 
        {
            perror("recv");
            break;
        }

        else if(size == 0)
            break;
        if(!strcmp(buf, "bye"))
            break;
    
        printf("%s\n", buf);
        bzero(buf, 1024);
    }

    printf("客户端 %d 退出\n", fd);
    close(fd);
}

int main()
{
    //创建socket,参数说明:
    //指定协议族:AF_INET(IPv4)和 AF_INET6(IPv6)
    //指定套接字使用的协议,通常为0,表示根据 domain 和 type 自动选择合适的协议。
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd) 
    {
        perror("socket");
        exit(1);
    }   
        
    //绑定
    //sockaddr_in 是一个用于表示 IPv4 地址的结构体
    struct sockaddr_in server_info; //用于保存服务器的信息(IP PROT)
    bzero(&server_info, sizeof(struct sockaddr_in)); //清空,bzero() 是一个函数,用于将指定内存区域的前几个字节设置为零
    server_info.sin_family = AF_INET; //地址族
    server_info.sin_port = htons(7000); //端口,大于1024都行
//  server_info.sin_addr.s_addr = inet_addr("127.0.0.1"); //表示回环IP,用于测试
    server_info.sin_addr.s_addr = inet_addr("192.168.175.129");
        
    if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
    {
        perror("bind");
        exit(2);
   }

    //设置监听队列
    if(listen(sockfd, 10) == -1)
    {
        perror("listen");
        exit(3);
    }

    printf("等待客户端的连接...\n");
    
    
    //接受连接
    struct sockaddr_in client_info;
    int length = sizeof(client_info);
    int fd;
    while(1)
    {
        fd = accept(sockfd, (struct sockaddr *)&client_info, &length);
        if(-1 == fd)
        {
            perror("accept");
            exit(4);
        }

        printf("接受客户端的连接 %d\n", fd);

        //为每个客户端创建一个线程
        pthread_t tid;
        if(pthread_create(&tid, NULL, client_recv, &fd) != 0)
        {
            perror("pthread_create");
            exit(4);  
        }
        
        pthread_detach(tid);
    
    }


//  close(fd); //关闭TCP连接,不能在接受数据
    close(sockfd); //关闭socket,不能再处理客户端的请求
    
    //socket用于处理客户端的连接,fd用于处理客户端的消息
    return 0}

四、服务器转发

服务器代码:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

typedef struct Info
{
    char text[1024];
    int tofd;
}Info;
                                                                                                                                                    
void *client_recv(void *arg)
{
    int fd = *(int *)arg;

    Info buf;
    ssize_t size;

    while(1)
    {   
        size = recv(fd, &buf, sizeof(buf), 0); 
        if(size == -1) 
        {   
            perror("recv");
            break;
        }   

        else if(size == 0)
            break;
        if(!strcmp(buf.text, "bye"))
            break;
        
        //转发数据
        if(send(buf.tofd, &buf, size, 0) == -1)
        {
            perror("send");
            break;
        }
        bzero(&buf, sizeof(buf));
    }

    printf("客户端 %d 退出\n", fd);
    close(fd);
}
    
int main()
{   
    //创建socket,参数说明:
    //指定协议族:AF_INET(IPv4)和 AF_INET6(IPv6)
    //指定套接字使用的协议,通常为0,表示根据 domain 和 type 自动选择合适的协议。
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {   
        perror("socket");
        exit(1);
    }       
        
    //绑定
    //sockaddr_in 是一个用于表示 IPv4 地址的结构体
  struct sockaddr_in server_info; //用于保存服务器的信息(IP PROT)
    bzero(&server_info, sizeof(struct sockaddr_in)); //清空,bzero() 是一个函数,用于将指定内存区域的前几个字节设置为零
    server_info.sin_family = AF_INET; //地址族
    server_info.sin_port = htons(7000); //端口,大于1024都行
//  server_info.sin_addr.s_addr = inet_addr("127.0.0.1"); //表示回环IP,用于测试
    server_info.sin_addr.s_addr = inet_addr("192.168.175.129");
        
    if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
    {       
        perror("bind");
        exit(2);
    }

    //设置监听队列
    if(listen(sockfd, 10) == -1)
    {
        perror("listen");
        exit(3);
    }
    
    printf("等待客户端的连接...\n");
    
    
    //接受连接
    struct sockaddr_in client_info;
    int length = sizeof(client_info);
    int fd;
    while(1)
    {   
        fd = accept(sockfd, (struct sockaddr *)&client_info, &length);
        if(-1 == fd)
       {
            perror("accept");
            exit(4);
        }

        printf("接受客户端的连接 %d\n", fd);
        
        //为每个客户端创建一个线程
        pthread_t tid;
        if(pthread_create(&tid, NULL, client_recv, &fd) != 0)
        {
            perror("pthread_create");
            exit(4);
        }
        
        pthread_detach(tid);
    
    }   
    
    
//  close(fd); //关闭TCP连接,不能在接受数据
    close(sockfd); //关闭socket,不能再处理客户端的请求
    
    //socket用于处理客户端的连接,fd用于处理客户端的消息
    return 0;

客户端代码:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

typedef struct Info
{
    char text[1024];                                                                                                                                
    int tofd;
}Info;
pthread_t tid[2] = {0};

void *send_thread(void *arg)
{
    int sockfd = *(int *)arg;
    Info buf;

    while(1)
    {   
        scanf("%s%d", buf.text, &buf.tofd);
        if(send(sockfd, &buf, sizeof(buf), 0) == -1) 
        {
            perror("send");
            break;
        }

        if(!strcmp(buf.text, "bye"))
            break;
        
        bzero(&buf, sizeof(buf));

    }
    
}

void *recv_thread(void *arg)
{
    int sockfd = *(int *)arg;
    Info buf;
    ssize_t size;

    while(1)
    {
        size = recv(sockfd, &buf, sizeof(buf), 0);
        if(size == -1)
        {
            perror("recv");
            break;
        }
        else if(size == 0)
            break; 
        
        printf("%s\n", buf.text);
        bzero(&buf, sizeof(buf));
    }
}       

int main()
{           
    //创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {
        perror("socket");
        exit(1);
    }

    //发起连接
    struct sockaddr_in server_info; // 存储服务器信息
    bzero(&server_info, 0);
    server_info.sin_family = AF_INET;  //地址族
    server_info.sin_port = htons(7000); //绑定端口
    server_info.sin_addr.s_addr = inet_addr("192.168.175.129"); //绑定ip地址
    if(connect(sockfd, (struct sockaddr*)&server_info, sizeof(server_info)) == -1)
    {   
        perror("connect");
        exit(2);
    }       

    //启动2个线程,一个负责发送,一个负责接收
    if(pthread_create(&tid[0], NULL, send_thread, &sockfd) != 0)
    {       
        perror("pthread_create");
        exit(3);
    }
    
    if(pthread_create(&tid[1], NULL, recv_thread, &sockfd) != 0)
    {
        perror("pthread_create")
        exit(4);
    }
    
    void *status;
    pthread_join(tid[0], &status);
    pthread_join(tid[1], &status);
 
    

    close(sockfd);
    return 0;
}   

五、UDP服务端

创建UDP服务器的步骤:

  1. 创建socket
  2. 绑定端口、地址等

代码:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
    //创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0); 
    if(-1 == sockfd)
    {   
        perror("socket");
        exit(1);
    }   

    //绑定
    struct sockaddr_in server_info;
    bzero(&server_info, sizeof(server_info));
    server_info.sin_family = AF_INET;
    server_info.sin_port = htons(6000);
    server_info.sin_addr.s_addr = inet_addr("127.0.0.1");
    if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1) 
    {   
        perror("bind");
        exit(2);
    }   

    ssize_t size;
    char buf[1024] = {0};
    struct sockaddr_in client_info;
    int len = sizeof(client_info);

    while(1)
    {
        size = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&client_info, &len);
        if(-1 == len)
        {
            perror("recvfrom");
            break;
        }

        printf("收到 %s : %d 的数据 %s\n", inet_ntoa(client_info.sin_addr), client_info.sin_port, buf);
        bzero(buf, 1024);
    }

    close(sockfd);
    return 0;
}

六、UDP客户端

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0); 
    if(sockfd == -1) 
    {   
        perror("socket");
        exit(1);
    }   

    char buf[1024] = {0};
    struct sockaddr_in server_info;
    bzero(&server_info, sizeof(server_info));
    server_info.sin_family = AF_INET;
    server_info.sin_port = htons(6000);
    server_info.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    while(1)
    {   
        scanf("%s", buf);
        ssize_t size = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&server_info, sizeof(server_info));
        if(-1 == size)
        {
            perror("send");
            break;
        }

        bzero(buf, 1024);
    }

    close(sockfd);
    return 0;
}

           	

七、高并发服务器select:select 是一个在 C 语言中使用的系统调用,用于实现 I/O 多路复用。它可以同时监视多个文件描述符,以确定它们中是否有可读、可写或异常等事件发生。

使用 select 的一般步骤如下:

  1. 创建并初始化 fd_set 集合,设置要监视的文件描述符。
  2. 调用 select 函数,传入需要监视的文件描述符集合和超时时间。
  3. 检查返回值,判断哪些文件描述符就绪。
  4. 根据就绪的文件描述符进行相应的操作。

select

select函数的原型:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数说明:

  • nfds:监视的文件描述符的最大值加 1。
  • readfds:指向一个 fd_set,包含要监视可读事件的文件描述符。如果为NULL,代表不监视,下2同。
  • writefds:指向一个 fd_set,包含要监视可写事件的文件描述符。
  • exceptfds:指向一个 fd_set,包含要监视异常事件的文件描述符。
  • timeout:指向一个 struct timeval 结构体,用于设置超时时间。如果为 NULL,则 select 将一直阻塞,直到有事件发生。

返回值:

  • 如果有事件发生或超时,select 返回就绪文件描述符的数量。
  • 如果出错,返回 -1,并设置 errno。

fd_set

概念: fd_set 是一个集合数据结构。它用于在 C 语言中表示一组文件描述符(file descriptor)的集合。
fd_set 是一个位向量(bit vector),用于表示文件描述符的集合。它通常是一个固定大小的数组,每个元素都是一个位字段(bit field),用于表示一个文件描述符的状态。每个位对应一个文件描述符,如果该位为 1,则表示对应的文件描述符在集合中,如果该位为 0,则表示对应的文件描述符不在集合中。

fd_set 的一些常用操作和相关的函数:

  • FD_ZERO(fd_set *set):将 fd_set 初始化为空集合,即清除所有的位。
  • FD_SET(int fd, fd_set *set):将指定的文件描述符 fd 添加到 fd_set 中,即将对应的位设置为 1。
  • FD_CLR(int fd, fd_set *set):从 fd_set 中移除指定的文件描述符 fd,即将对应的位清除为 0。
  • FD_ISSET(int fd, fd_set *set):检查指定的文件描述符 fd 是否在 fd_set 中,即对应的位是否为 1。
  • FD_COPY(fd_set *src, fd_set *dest):复制一个 fd_set,将 src 中的位复制到 dest 中。

代码实例

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <string.h>

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0); 
    if(-1 == sockfd)
    {   
        perror("socket");
        exit(1);
    }   

    struct sockaddr_in server_info;
    bzero(&server_info, sizeof(server_info));
    server_info.sin_family = AF_INET;
    server_info.sin_port = htons(7000);
    server_info.sin_addr.s_addr = inet_addr("192.168.175.129");
    if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1) 
    {   
        perror("bind");
        exit(2);
    }   

    if(listen(sockfd, 10) == -1) 
    {   
       perror("listen");
        exit(3);
    }

    printf("等待客户端的连接...\n");

    //定义两个集合
    fd_set readset, tmpset;

    //初始化
    FD_ZERO(&readset);
    FD_ZERO(&tmpset);

    //添加文件描述符
    FD_SET(sockfd, &readset);

    int maxfd = sockfd;
    int fd[1024] = {0};
    int ret, i;

    while(1)
    {
        tmpset = readset;
        ret = select(maxfd + 1, &tmpset, NULL, NULL, NULL);

        if(-1 == ret)
        {
            perror("select");
            break;
        }

        if(FD_ISSET(sockfd, &tmpset)) //判断sockfd是否留在集合中(是否有客户端发起连接)
        {
            struct sockaddr_in client_info; //用于保存客户端信息
            int length = sizeof(client_info);

            //找到一个没有分配的fd
            for(i = 0; i < 1024; i++)
            {
                if(0 == fd[i])
                    break;
            }

            fd[i] = accept(sockfd, (struct sockaddr *)&client_info, &length);
            if(-1 == fd[i])
            {
                perror("accept");
                break;
            }

            printf("接受客户端的连接:%d\n", fd[i]);
            //把新的文件描述符加入集合中
            FD_SET(fd[i], &readset);
            //更新最大文件描述符
            if(maxfd < fd[i])
                maxfd = fd[i];

        }

        else
        {
           for(i = 0; i < 1024; i++)
            {
                if(FD_ISSET(fd[i], &tmpset))
                {
                    char buf[1024] = {0};
                    ssize_t size;

                    size = recv(fd[i], buf, sizeof(buf), 0);
                    if(size == -1)
                    {
                        perror("recv");
                    }
                    else if(size == 0)
                    {
                        printf("客户端 %d 退出\n", fd[i]);
                        FD_CLR(fd[i], &readset); // 从集合中删掉
                        close(fd[i]);
                        fd[i] = 0;
                    }
                    else
                        printf("%s\n", buf);

                    break;
                }
            }
        }
    }

    close(sockfd);
    return 0;
}

八、高并发服务器epoll

概念: epoll 是一个在 Linux 操作系统上用于高效事件通知的 I/O 多路复用机制。它是一种替代传统的 select 和 poll 函数的方法,可以在大规模并发连接的网络服务器中提供更高的性能。

使用 epoll 的一般步骤如下:

  1. 创建一个 epoll 实例:
int epoll_create(int size);

这个函数会返回一个文件描述符,用于后续的 epoll 操作。

  1. 向 epoll 实例中添加文件描述符:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfd 是 epoll 实例的文件描述符,op 是操作类型(如添加、修改或删除),fd 是要添加的文件描述符,event 是一个 epoll_event 结构体,用于指定事件类型和关联的数据

  1. 等待事件的发生:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epfd 是 epoll 实例的文件描述符,events 是一个数组,用于存储发生的事件,maxevents 是数组的大小,timeout 是等待的超时时间(-1 表示无限等待)。

  1. 处理发生的事件:
    遍历 epoll_wait 返回的事件数组,根据事件类型进行相应的处理。

  2. 关闭 epoll 实例

int close(int fd);

1.epoll_create

概念: epoll_create 函数用于创建一个 epoll 实例,并返回一个文件描述符,以便后续的 epoll 操作。
它的函数原型如下:

int epoll_create(int size);

参数说明:

  • size:指定 epoll 实例的大小,它是一个整数,通常可以设置为大于 0 的任意值。这个参数在较早的内核版本中被忽略,可以将其设置为 0。

返回值:

  • 如果成功,返回一个非负整数,表示 epoll 实例的文件描述符(epoll 文件描述符)。
  • 如果出错,返回 -1,并设置 errno。

2.epoll_event

epoll_event 是一个结构体类型,在 Linux 中使用 epoll 进行 I/O 多路复用时,用于表示事件的数据结构。
它的定义如下:

struct epoll_event {
    uint32_t events;  // 表示事件类型的位掩码
    epoll_data_t data;  // 与事件相关的用户数据
};
  • events:一个无符号 32 位整数,用于表示事件类型的位掩码。可以使用以下常量进行设置:

    • EPOLLIN:表示可读事件(有数据可读)。
    • EPOLLOUT:表示可写事件(可以写入数据)。
    • EPOLLRDHUP:表示对端关闭连接或关闭了写入一半的连接。
    • EPOLLERR:表示错误事件。
    • EPOLLHUP:表示挂起事件(连接被挂起)。
    • EPOLLET:使用边缘触发模式(Edge-Triggered)。
    • EPOLLONESHOT:一次性事件,只会触发一次。
    • 默认水平触发
  • data:一个 epoll_data_t 类型的联合体,用于存储与事件相关的用户数据。epoll_data_t 的定义如下:

typedef union epoll_data {
    void *ptr;  // 指针类型的用户数据
    int fd;     // 文件描述符类型的用户数据
    uint32_t u32;  // 32 位无符号整数类型的用户数据
    uint64_t u64;  // 64 位无符号整数类型的用户数据
} epoll_data_t;

3.epoll_ctl

epoll_ctl 是 Linux 中使用 epoll 进行事件注册和控制的系统调用。它用于向 epoll 实例中添加、修改或删除事件。
epoll_ctl 的原型如下:

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数说明:

  • epfd:epoll 实例的文件描述符(即 epoll 文件描述符),通过 epoll_create 创建并返回。
  • op:操作类型,可以是以下值之一:
    • EPOLL_CTL_ADD:向 epoll 实例中添加事件。
    • EPOLL_CTL_MOD:修改 epoll 实例中的事件。
    • EPOLL_CTL_DEL:从 epoll 实例中删除事件。
  • fd:要操作的文件描述符,用于指定要添加、修改或删除的事件所属的文件描述符。
  • event:指向 struct epoll_event 结构体的指针,用于指定要添加、修改或删除的事件的详细信息。

返回值:

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

4.epoll_wait

epoll_wait 是 Linux 中使用 epoll 进行事件等待和处理的系统调用。它用于等待 epoll 实例中注册的事件发生,并返回就绪的事件信息。
epoll_wait 的原型如下:

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数说明:

  • epfd:epoll 实例的文件描述符(即 epoll 文件描述符),通过 epoll_create 创建并返回。
  • events:指向 struct epoll_event 数组的指针,用于存储就绪的事件信息。
  • maxevents:events 数组的大小,即最多可以存储多少个事件信息。
  • timeout:等待的超时时间(以毫秒为单位),可以设置为以下值之一:
    • -1:无限等待,直到有事件发生。
    • 0:立即返回,不等待任何事件。
    • 大于 0:等待指定的毫秒数后返回,如果没有事件发生则超时。

返回值:

  • 如果成功,返回就绪的事件数量。
  • 如果超时,返回 0。
  • 如果出错,返回 -1,并设置 errno。

5.代码实例

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {
        perror("socket");
        exit(1);
    }

    struct sockaddr_in server_info;
    bzero(&server_info, sizeof(server_info));
    server_info.sin_family = AF_INET;
    server_info.sin_port = htons(7000);
    server_info.sin_addr.s_addr = inet_addr("192.168.175.129");

    if(bind(sockfd, (struct sockaddr *)&server_info, sizeof(server_info)) == -1)
    {
        perror("bind");
        exit(2);
    }

    if(listen(sockfd, 10) == -1)
    {
      perror("listen");
        exit(3);
    }

    printf("等待客户端连接...\n");

    //创建epoll对象(创建集合)
    int epfd = epoll_create(1);
    if(-1 == epfd)
    {
        perror("epoll_create");
        exit(4);
    }

    //把sockfd封装成事件,添加到集合中
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET; //事件属性,边缘触发
    ev.data.fd = sockfd;
    if(epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1)
    {
        perror("epoll_ctl");
        exit(5);
    }

    while(1)
    {
        struct epoll_event events[10];
        int num = epoll_wait(epfd, events, 10, -1);
        if(-1 == num)
        {
            perror("epoll_wait");
            break;
        }
    
        for(int i = 0; i < num; i++)
        {
            if(events[i].data.fd == sockfd) //有客户端发起连接
            {
                struct sockaddr_in server_info;
                int length = sizeof(server_info);
                int fd = accept(sockfd, (struct sockaddr *)&server_info, &length);
                if(-1 == fd)
                {
                    perror("accept");
                    break;
                }
    
                printf("客户端 %d 连接成功\n", fd);
    
                ev.events = EPOLLIN; //默认水平触发
                ev.data.fd = fd;
                if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
                {
                    perror("epoll_ctl");
                    break;
                }
            }
            else
            {
                char buf[1024] = {0};
                ssize_t size;
               size = recv(events[i].data.fd, buf, sizeof(buf), 0);
                if(size == -1)
                    perror("recv");
                else if(size == 0)
                {
                    if(epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &events[i]) == -1)
                    {
                        perror("epoll_ctl");
                        exit(5);
                    }

                    close(events[i].data.fd);
                    printf("客户端 %d 退出\n", events[i].data.fd);
                }
                else
                    printf("%s\n", buf);
            }
        }

    }

    close(sockfd);
    return 0;
}

九、并发服务器的总结

在这里插入图片描述

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值