网络编程IO复用方法

一、select

1. select接口

IO复用的作用:
在这里插入图片描述

在这里插入图片描述
select的返回值是fd_set中产生事件的文件描述符的个数

fd_set就是一个长整型的数组,使用每个bit标记一个文件描述符,最大为FD_SETSIZE个位,默认为1024,select能监听描述符的上限

一般的,我们使用宏来操作fd_set中的bit

用select可以监听很多个socket,但每次都需要遍历socket数组,把感兴趣的socket设置到一个位数组中,然后把位数组传递给select,即传递到内核空间
事件发生后,select会将没有发生事件的socket对应的位置为0,仅保留发生事件的socket在位数组中。将位数组从内核空间拷贝到用户空间,应用程序需要遍历所有的socket,判断当前socket是否在位数组中,进行read和accept

#include<sys/select.h>
FD_ZERO(fd_set* fd_set);             // 清楚fd_set的所有位
FD_SET(int fd, fd_set* fdset)        // 设置fd_set的位fd
FD_CLR(int fd, fd_set* fdset)        // 清除fd_set的位fd
int FD_ISSET(int fd, fd_set* fdset)  // 测试fd_set的位fd是否被设置
2. select完整代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<arpa/inet.h>

#define MAX 10

int socket_init(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1){
        return -1;
    }
    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0 ,sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(8888);
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    assert(res != -1);

    res = listen(sockfd, 5);
    assert(res != -1);

    return sockfd;
}

void fds_init(int* fds){
    if(NULL == fds){
        return ;
    }
    for(int i = 0; i < MAX; i++){
        fds[i] = -1;
    }
}

void fds_del(int* fds, int fd){
    if(NULL == fds){
        return ;
    }
    for(int i = 0; i < MAX; i++){   
        if(fds[i] == fd){
            fds[i] = -1;
            break;
        }
    }
}

void fds_add(int* fds, int fd){
    if(NULL == fds){
        return ;
    }
    for(int i = 0; i < MAX; i++){
        if(fds[i] == -1){
            fds[i] = fd;
            break;
        }
    }
}

int main(){
    int sockfd = socket_init();

    int fds[MAX];  // 存放可能有事件发生的描述符,比如 stdin : 0, stdout : 1, stderr : 2
    fds_init(fds);
    fds_add(fds, sockfd);

    fd_set read_fdset;  // 给select检测的位数组
    
    while(1){
        FD_ZERO(&read_fdset);
        int maxfd = -1;   // 记录描述符的最大值,使select只需要检测位数组read_fdset的前maxfd+1位

        // 遍历fds,把可能发生读事件的文件描述符放入read_fdset
        for(int i = 0; i < MAX; i++){
            if(fds[i] == -1){
                continue;
            }
            FD_SET(fds[i], &read_fdset);
            maxfd =  fds[i] > maxfd ? fds[i] : maxfd;
        }

        struct timeval tv = {5, 0};
        // select前read_fdset是可能发生事件的集合
        // select后read_fdset被内核修改为发生事件的集合,无事件的描述符被删除
        int n = select(maxfd + 1, &read_fdset, NULL, NULL, &tv);
        if( n == -1 ){
            continue;
        }else if(n == 0){
            printf("select timeout!\n");
        }else{
            // 找到发生事件的n个描述符
            for(int i = 0; i < MAX; i++){
                if(fds[i] == -1){
                    continue;
                }
                if(FD_ISSET(fds[i], &read_fdset)){
                    // 服务器可能有监听事件 和 接收数据的事件
                    if(fds[i] == sockfd){
                        struct sockaddr_in cli_addr;
                        int len = sizeof(cli_addr);
                        int conn = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
                        if(conn < 0){
                            printf("客户端连接失败!\n");
                            continue;
                        }
                        printf("客户端:%d 连接成功\n", conn);
                        fds_add(fds, conn);
                    }else{
                        char buff[128];
                        memset(buff, 0, 128);
                        int num = recv(fds[i], buff, 127, 0);
                        if(num <= 0){
                            printf("客户端:%d 关闭\n", fds[i]);
                            close(fds[i]);
                            fds_del(fds, fds[i]);
                            continue;   // 当前描述符关闭,再检测下一个描述符
                        }
                        printf("read:%s\n", buff);
                        send(fds[i], buff, num, 0);
                    }
                }
            }
        }
    } 
    return 0;
}

二、Poll

1. poll接口

poll 系统调用和 select 类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者

 int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • poll 系统调用成功返回就绪文件描述符的总数,超时返回 0,失败返回-1
  • nfds 参数指定被监听事件集合 fds 的大小。
  • timeout 参数指定 poll 的超时值,单位是毫秒,timeout 为-1 时,poll 调用将永久阻塞,直到某个事件发生,timeout 为 0 时,poll 调用将立即返回。

fds 参数是一个 struct pollfd 结构类型的数组,它指定所有用户感兴趣的文件描述符上发生的可读、可写和异常等事件。pollfd 结构体定义如下:

struct pollfd{
   int fd; // 文件描述符
   short events; // 注册的关注事件类型
   short revents; // 实际发生的事件类型,由内核填充
};

其中:

  • fd 成员指定文件描述符
  • events 成员告诉 poll 监听 fd 上的哪些事件类型,它是一系列事件的按位或
  • revents 成员则有内核修改,通知应用程序 fd上实际发生了哪些事件
2. poll支持的事件类型

在这里插入图片描述

poll链表保存文件描述符,因此没有了监视文件数量的限制(1024)

每次调用poll,需要把用户空间的pollfd数组传入poll,即拷贝到内核空间,如果有pollfd 发生了事件,内核会修改数组元素的revents成员,poll返回时又需要把pollfd 数组从内核空间拷贝到用户空间
应用程序遍历整个pollfd 数组,查看该数组元素的revents成员,进行相应的read或accept

3. poll完整代码
#define _GNU_SOURCE
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<poll.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<arpa/inet.h>

#define MAX 10

int socket_init(){
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == -1){
        return -1;
    }
    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0 ,sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(8888);
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    assert(res != -1);

    res = listen(listenfd, 5);
    assert(res != -1);

    return listenfd;
}

// struct pollfd fds[]中存放所有的描述符,根据revents判断是否有事件发生
void fds_init(struct pollfd fds[]){
    if(NULL == fds){
        return ;
    }
    for(int i = 0; i < MAX; i++){
        fds[i].fd = -1;
        fds[i].events = 0;
        fds[i].revents = 0;
    }
}

void fds_add(struct pollfd fds[], int fd){
    if(NULL == fds){
        return ;
    }
    for(int i = 0; i < MAX; i++){
        if( -1 == fds[i].fd ){
            fds[i].fd = fd;
            fds[i].events = POLLIN | POLLRDHUP; // read事件
            fds[i].revents = 0;     // 这由内核填充
            break;
        }
    }
}

void fds_del(struct pollfd fds[], int fd){
    if(NULL == fds){
        return ;
    }
    for(int i = 0; i < MAX; i++){   
        if(fd == fds[i].fd){
            fds[i].fd = -1;
            fds[i].events = 0; 
            fds[i].revents = 0;
            break;
        }
    }
}

int main(){
    int listenfd = socket_init();

    struct pollfd fds[MAX]; // 存放可能发生事件的描述符
    fds_init(fds);
    fds_add(fds, listenfd);
    
    while(1){
        int n = poll(fds, MAX, 5000);  // 等待5s无事件,则下一轮循环,有事件则处理事件
        if(n == -1){
            printf("poll失败!\n");
            continue;
        }else if(n == 0){
            printf("poll timeout!\n");
            continue;
        }else{
            // 找到发生事件的n个描述符
            for(int i = 0; i < MAX; i++){
                if(fds[i].fd == -1){
                    continue;
                }
                // 一旦客户端关闭,都会收到POLLRDHUP事件
                if(fds[i].revents & POLLRDHUP){
                    fds_del(fds, fds[i].fd);
                    close(fds[i].fd);
                    continue;
                }
                // 由于只是设置了读事件,这里只检查读事件
                if(fds[i].revents & POLLIN){
                    if(listenfd == fds[i].fd){
                        // accept处理
                        struct sockaddr_in cli_addr;
                        int len = sizeof(cli_addr);
                        int conn = accept(listenfd, (struct sockaddr*)&cli_addr, &len);
                        if(conn < 0){
                            printf("客户端连接失败!\n");
                            continue;
                        }
                        printf("client %s:%d 连接成功,使用的描述符:%d\n", 
                            inet_ntoa(((struct sockaddr_in)cli_addr).sin_addr), 
                            ntohs(((struct sockaddr_in)cli_addr).sin_port), 
                            conn);
                        fds_add(fds, conn);
                    }else{
                        char buff[128] = {0};
                        int num = recv(fds[i].fd, buff, 127, 0);
                        if(num <= 0){
                            printf("客户端:%d 关闭\n", fds[i].fd);
                            fds_del(fds, fds[i].fd);
                            close(fds[i].fd);
                            continue;   // 当前描述符关闭,再检测下一个描述符
                        }
                        printf("buff(%d):%s\n",num, buff);
                        send(fds[i].fd, buff, num, 0);
                    }
                }
            }
        }
    } 
    return 0;
}

三、epoll

1. epoll接口

epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、poll 有很大差异。首先,epoll 使用一组函数来完成任务,而不是单个函数。其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像select和poll那样每次调用都要重复传入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。epoll是一组方法的总称,包括:

  • epoll_create(int size): 用于创建内核事件表,底层为红黑树。成功返回内核事件表的文件描述符,失败返回-1。size 参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大,传入的实参合法即可。

  • epoll_ctl(int epfd, int op, int fd, struct epoll_event *event): 用于操作内核事件表
    在这里插入图片描述

  • epoll_wait(): 用于在一段超时时间内等待一组文件描述符上的事件
    在这里插入图片描述

把原先的select/poll调用分成以下3个部分:

  • 调用epoll_create立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
  • 调用epoll_ctl向epoll对象中添加这100万个连接的套接字,以红黑树的形式组织,增删查是 O ( l o g 2 N ) O(log_2N) O(log2N)
  • 调用epoll_wait收集发生的事件的fd资源,发生事件的fd会从红黑树上拷贝到双向循环链表,用于返回给用户。用户拿到的就只有发生事件的文件描述符了

不需要每次调用epoll_wait时,都把整个感兴趣的fd数组传入,只需要传入一个用于存放发生事件fd的epoll_event 数组即可

2. epoll完整代码
#define _GNU_SOURCE
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<poll.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<arpa/inet.h>

#define MAX 10

int socket_init(){
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == -1){
        return -1;
    }
    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0 ,sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(8888);
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    assert(res != -1);

    res = listen(listenfd, 5);
    assert(res != -1);

    return listenfd;
}

// epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
// epoll_fd:内核事件表的id,把描述符添加到内核事件表中,用于后面检测是否发生事件
void epoll_add(int epoll_fd, int fd){
    struct epoll_event event;  // 将fd封装成结构体后,放入内核事件表
    event.events = EPOLLIN | EPOLLRDHUP;    // 在内核事件表中注册事件
    event.data.fd = fd;        // 在内核事件表中注册描述符
    if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1){
        perror("epoll_ctl add error\n");
    }
}

// 当客户端关闭连接,则把相应描述符从内核事件表中移除
void epoll_del(int epoll_fd, int fd){
    if(epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1){
        perror("epoll_ctl delete error\n");
    }
}

int main(){
    int listenfd = socket_init();
    
    // 创建内核事件表
    int epoll_fd = epoll_create(MAX);
    assert(epoll_fd != -1);
    
    // 添加描述符到内核事件表
    epoll_add(epoll_fd, listenfd);

    // 一次最多获取MAX个有事件的描述符,若有事件的描述符过多,则分多次获取
    // 存放有事件的描述符
    struct epoll_event events[MAX];
    while(1){
        // int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
        // 从内核事件表epoll_fd中获取有事件的描述符以struct epoll_event的形式放在events
        // epoll_wait的返回值不大于MAX
        int n = epoll_wait(epoll_fd, events, MAX, 5000);
        if(n == -1){
            perror("epoll wait error\n");
        }else if(n == 0){
            perror("epoll timeout\n");
        }else{
            // select和poll需要遍历所有的文件描述符
            // 而epoll不需要遍历所有的,只需要前n个元素即可
            for(int i = 0; i < n; i++){
                int cur_fd = events[i].data.fd;
                // 一旦客户端关闭,都会收到POLLRDHUP事件
                if(events[i].events & POLLRDHUP){
                    printf("client:%d hup close\n", cur_fd);
                    epoll_del(epoll_fd, cur_fd);
                    close(cur_fd);
                    continue;
                }
                if(events[i].events & EPOLLIN){
                    if(cur_fd == listenfd){
                        // accept
                        struct sockaddr_in cli_addr;
                        int len = sizeof(cli_addr);
                        int conn = accept(listenfd, (struct sockaddr*)&cli_addr, &len);
                        if(conn < 0){
                            printf("客户端连接失败!\n");
                            continue;
                        }
                        printf("client %s:%d 连接成功,使用的描述符:%d\n", 
                            inet_ntoa(((struct sockaddr_in)cli_addr).sin_addr), 
                            ntohs(((struct sockaddr_in)cli_addr).sin_port), 
                            conn);
                        // 新的描述符添加到内核事件表
                        epoll_add(epoll_fd, conn);
                    }else{
                        // recv
                        char buff[128] = {0};
                        int num = recv(cur_fd, buff, 127, 0);
                        if(num <= 0){
                            printf("客户端:%d 关闭\n", cur_fd);
                            epoll_del(epoll_fd, cur_fd);
                            close(cur_fd);
                            continue;   // 当前描述符关闭,再检测下一个描述符
                        }
                        printf("buff(%d):%s\n",num, buff);
                        send(cur_fd, buff, num, 0); 
                    }
                }
            }
        }
    }
    return 0;
}

在这里插入图片描述

四、LT和ET模式

1. LT和ET的基本概念

ET模式(边沿触发)的文件描述符(fd):

当epoll_wait检测到fd上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。

epoll_wait只有在客户端第一次发数据是才会返回,以后即使缓冲区里还有数据,也不会返回了。epoll_wait是否返回,是看客户端是否发数据,客户端发数据了就会返回,且只返回一次。

eg:客户端发送数据,I/O函数只会提醒一次服务端fd上有数据,以后将不会再提醒

所以要求服务端必须一次把数据读完--->循环读数据 (读完数据后,可能会阻塞)  --->将描述符设置成非阻塞模式

LT模式(水平触发)的文件描述符(fd):

当epoll_wait检测到fd上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通知此事件,直到此事件被处理。

eg:客户端发送数据,I/O函数会提醒描述符fd有数据---->recv读数据,若一次没有读完,I/O函数会一直提醒服务端fd上有数据,直到recv缓冲区里的数据读完

可见ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此ET模式效率比LT模式高

原因:ET模式下事件被触发的次数比LT模式下少很多

注意:每个使用ET模式的文件描述符都应该是非阻塞的。 如果描述符是阻塞的,那么读或写操作将会因没有后续事件而一直处于阻塞状态

在这里插入图片描述

epoll默认处于LT模式,也就是内核会不断提醒应用程序,直到程序处理完数据

修改服务器的recv:

int num = recv(cur_fd, buff, 1, 0);

在这里插入图片描述

2. ET的epoll服务器完整代码
  1. 文件描述符开启 ET模式
  2. recv设置为 非阻塞,缓冲区没数据的时候返回-1,而不是阻塞
  3. 循环读取 缓冲区的数据,直到读完
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>

#define RD_FIN_STR "服务器数据读取完成\n"
#define MAX 10

int socket_init(){
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == -1){
        return -1;
    }
    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0 ,sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(8888);
    ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    assert(res != -1);

    res = listen(listenfd, 5);
    assert(res != -1);

    return listenfd;
}

void set_nonblock(int fd){
    // 获取文件描述符fd的属性
    int old_feature = fcntl(fd, F_GETFL);
    int new_feature = old_feature | O_NONBLOCK;
    // 将新属性设置到fd
    if(fcntl(fd, F_SETFL, new_feature) == -1){
        perror("fcntl error\n");
    }
}

// epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
// epoll_fd:内核事件表的id,把描述符添加到内核事件表中,用于后面检测是否发生事件
void epoll_add(int epoll_fd, int fd){
    struct epoll_event event;  // 将fd封装成结构体后,放入内核事件表
    event.events = EPOLLIN | EPOLLRDHUP | EPOLLET;    // 在内核事件表中注册事件
    event.data.fd = fd;        // 在内核事件表中注册描述符
    set_nonblock(fd);          // 将描述符设置为非阻塞

    if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1){
        perror("epoll_ctl add error\n");
    }
}

// 当客户端关闭连接,则把相应描述符从内核事件表中移除
void epoll_del(int epoll_fd, int fd){
    if(epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1){
        perror("epoll_ctl delete error\n");
    }
}


int main(){
    int listenfd = socket_init();
    
    // 创建内核事件表
    int epoll_fd = epoll_create(MAX);
    assert(epoll_fd != -1);
    
    // 添加描述符到内核事件表
    epoll_add(epoll_fd, listenfd);

    // 一次最多获取MAX个有事件的描述符,若有事件的描述符过多,则分多次获取
    // 存放有事件的描述符
    struct epoll_event events[MAX];
    while(1){
        // int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
        // 从内核事件表epoll_fd中获取有事件的描述符以struct epoll_event的形式放在events
        // epoll_wait的返回值不大于MAX
        printf("开始epoll_wait\n");
        int n = epoll_wait(epoll_fd, events, MAX, 5000);
        if(n == -1){
            perror("epoll wait error\n");
        }else if(n == 0){
            perror("epoll timeout\n");
        }else{
            // select和poll需要遍历所有的文件描述符
            // 而epoll不需要遍历所有的,只需要前n个元素即可
            for(int i = 0; i < n; i++){
                int cur_fd = events[i].data.fd;
                // 一旦客户端关闭,都会收到POLLRDHUP事件
                if(events[i].events & POLLRDHUP){
                    printf("client:%d hup close\n", cur_fd);
                    epoll_del(epoll_fd, cur_fd);
                    close(cur_fd);
                    continue;
                }
                if(events[i].events & EPOLLIN){
                    if(cur_fd == listenfd){
                        // accept
                        struct sockaddr_in cli_addr;
                        int len = sizeof(cli_addr);
                        int conn = accept(listenfd, (struct sockaddr*)&cli_addr, &len);
                        if(conn < 0){
                            printf("客户端连接失败!\n");
                            continue;
                        }
                        printf("client %s:%d 连接成功,使用的描述符:%d\n", 
                            inet_ntoa(((struct sockaddr_in)cli_addr).sin_addr), 
                            ntohs(((struct sockaddr_in)cli_addr).sin_port), 
                            conn);
                        // 新的描述符添加到内核事件表
                        epoll_add(epoll_fd, conn);
                    }else{
                        // recv
                        while(1){
                            char buff[128] = {0};
                            // fd已经被设置为非阻塞,读取不到不会阻塞,而回返回-1
                            int num = recv(cur_fd, buff, 1, 0);
                            if(num == -1){
                                // 不是因为数据读完而出错
                                // errno是errno.h中的一个全局变量,如果读取出错,内核会填写errno
                                if(errno != EAGAIN && errno != EWOULDBLOCK){
                                    perror("recv error\n");
                                }else{
                                    // 数据读完导致num == -1
                                    printf("数据读取完成\n");
                                    send(cur_fd, RD_FIN_STR, sizeof(RD_FIN_STR), 0);
                                }
                                break; // 数据读完或出错,退出接收数据
                            }else if(num == 0){
                                printf("客户端:%d 关闭\n", cur_fd);
                                epoll_del(epoll_fd, cur_fd);
                                close(cur_fd);
                                break;   // 客户端关闭,退出接收数据
                            }else{
                                printf("buff(%d):%s\n",num, buff);
                                send(cur_fd, buff, num, 0); 
                            }
                        }
                    }
                }
            }
        }
    }
    return 0;
}
3. 读取完缓冲区的客户端完整代码
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>

void set_nonblock(int fd){
    // 获取文件描述符fd的属性
    int old_feature = fcntl(fd, F_GETFL);
    int new_feature = old_feature | O_NONBLOCK;
    // 将新属性设置到fd
    if(fcntl(fd, F_SETFL, new_feature) == -1){
        perror("fcntl error\n");
    }
}

int main(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1){
        printf("create socket failed!\n");
        return 0;
    }
    struct sockaddr_in cli_addr;
    memset(&cli_addr, 0, sizeof(cli_addr));

    cli_addr.sin_family = AF_INET;    // 地址族
    cli_addr.sin_port = htons(8888);  // host to net short
    cli_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // inet_addr将字符串转为无符号整型

    // 可以将套接字绑定ip,但一般客户端不绑定,让OS随机分配port
    int res = connect(sockfd, (struct sockaddr*)&cli_addr, sizeof(cli_addr));  // 连接server
    assert(res != -1);
    set_nonblock(sockfd);

    while(1){
        char buff[128] = {0};
        printf("input:");
        fflush(stdout);
        fgets(buff, 128, stdin);
        if(strcmp(buff, "exit\n") == 0){
            break;
        }
        send(sockfd, buff, strlen(buff), 0);
        while(1){
            memset(buff, 0 ,128);
            // 客户端send后,给服务器充足的时间读取以及回复
            sleep(1);
            // 从缓冲区尽可能多的取数据,最多取127字节
            int n = recv(sockfd, buff, 127, 0);
            if(n == -1){
                // 不是因为数据读完而出错
                // errno是errno.h中的一个全局变量,如果读取出错,内核会填写errno
                if(errno != EAGAIN && errno != EWOULDBLOCK){
                    perror("recv error\n");
                }
                // else{
                //     printf("服务器读完了,break\n");
                // }
                break; // 数据读完或出错,退出接收数据
            }else if(n == 0){
                printf("服务端:%d 关闭\n", sockfd);
                break;   // 客户端关闭,退出接收数据
            }else{
                printf("buff(%d):%s\n",n, buff);
            }
        }
    }
    close(sockfd);
    return 0;
}

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bugcoder-9905

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值