简易TCP服务器通信、IO多路复用(select、poll、epoll)以及reactor模式。

网络编程学习

  • 简单TCP服务器通信
    • 三次握手和四次挥手状态转换总结
    • client和server通信写法
      • server端
      • client端
    • 怎么应对多用户连接?
      • 缺点
  • IO多路复用
    • select
      • 优缺点
    • poll
      • poll写法和改进点
    • epoll(使用最多,重中之重)
      • epoll写法和改进点
      • LT模式和ET模式
        • **LT模式**
        • **ET模式**:
        • LT模式如何解决数据保存问题?
  • reactor模式
    • 针对IO处理的两种写法
  • 思考题
    • main函数如何被执行的?
    • time_wait怎么产生的?如果有大量的time_wait是为什么?
    • close_wait怎么产生的?如果有大量的close_wait是为什么?
    • epoll里是否使用了mmap?
    • epoll是否是线程安全的?

简单TCP服务器通信

三次握手和四次挥手状态转换总结

  • 详细见这篇

client和server通信写法

server端

  • 服务端编码步骤:
    1. 调用socket函数,获取一个sockfd(本质上是一个文件描述符,受到系统能打开文件数量上限限制),int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    2. 调用bind函数,将sockfd和ip、端口号绑定
      1. 创建一个sockaddr_in结构体变量,通过改结构体设置服务器的IP地址和端口号(这里要调用htonl和htons转换字节序)。
      2. 调用bind函数,绑定sockfd和ip、端口号;bind(sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr))
    3. 调用listen函数去监听sockfd上的请求。listen(sockfd, 8)
    4. 调用accept函数(默认为堵塞)去接收客户端请求。
      1. 创建两个变量,一个是sockaddr_in结构体变量,存储用来通信的客户端信息,一个是len代表结构体变量的大小。
      2. 调用accept函数,函数返回一个fd(可以用来读写数据的fd),int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
    5. 第五步就是一个while循环,然后循环里调用recv和send函数去读写数据。
  • 整体代码如下:
       int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(struct sockaddr_in));//赋值为空server_addr.sin_family = AF_INET;//IPV4协议server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//本地地址,如果有多个IP不知道设置为哪个就可以用这个参数。server_addr.sin_port = htons(2204);if (-1 == bind(sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr))) {perror("bind error");return -1;}if (-1 == listen(sockfd, 8)) {perror("listen error");return -1;}//acceptstruct sockaddr_in client_addr;int len = sizeof(struct sockaddr);//clientfd用于收发数据的int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);printf("clientfd: %d, serverfd: %d\n", clientfd, sockfd);while (1) {//receivechar buf[128] = {0};int recv_len = recv(clientfd, buf, 128, 0);printf("accept len: %d", recv_len);if (recv_len == -1){perror("recv error");break;} else if (recv_len == 0) {break;} else {printf("recv success\n");}//sendprintf("send");send(clientfd, buf, recv_len, 0);printf("send success");}
    

client端

client写法步骤:
1. 调用socket函数,获取一个sockfd(本质上是一个文件描述符),int sockfd = socket(AF_INET, SOCK_STREAM, 0);
2. 调用connect函数建立连接(这一步完成三次握手)
1. 创建一个sockaddr_in结构体变量,通过改结构体设置服务器的IP地址和端口号(这里要调用htonl和htons转换字节序)。
2. 调用connect函数,如果成功返回那么就可以直接用sockfd进行通信了。
3. 使用sockfd进行数据读写。

  • 整体代码如下:
    uint32_t lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd == -1) {perror("create socket");exit(-1);}//connect 172.17.71.122uint32_t dst = 0;inet_pton(AF_INET, "8.141.4.79", &dst);struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;//IPV4协议server_addr.sin_addr.s_addr = dst;server_addr.sin_port = htons(2204);if (connect(lfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("connect");exit(-1);}while (1) {for (int i = 0; i < 20; ++i) {const char* str = std::to_string(i).c_str();int write_fd = write(lfd, str, sizeof(str));char receive[1024] = "";int len1 = read(lfd, receive, sizeof(receive));if (len1 < 0) {perror("client read");exit(-1);} else if (len1 == 0) {printf("server closed!!!");break;} else {printf(" client read success: %s\n", receive);} sleep(1);}sleep(2);}

怎么应对多用户连接?

  • 上面的简单通信过程只能一次处理一个用户,我们如果想要处理多用户连接可以采用多线程的方式。
  • 方法也比较简单,就是将IO操作以及业务处理逻辑放到一个函数里,然后accept只要返回了,那么就创建一个线程去执行回调函数。

缺点

  1. 当并发量特别大的时候,服务器会受到内存的影响,性能并不高,无法适应高并发场景。
  2. 为此,我们需要学习下面的IO多路复用,采用一个线程去处理多个请求。

IO多路复用

  • 简单来讲就是可以用一个线程去同时监听多个请求,并一起返回,同时处理,减少线程资源消耗。

select

  • select相当于一个代理,去帮我们检测有哪些fd有事件发生,然后返回给我们一个数组,我们需要遍历数组依次处理每个fd。

  • 实现步骤如下:

    1. 定义一个fd_set变量和maxfd变量。
      1. fd_set变量是一个结构体变量,内部存储了一个fd数组,然后我们调用FD_SET函数将sockfd添加到fd_set里。
      2. maxfd变量存储当前的最大fd值,初始值为sockfd,后续会及时更新。
    2. 调用select函数去监听事件。int ret_code = select(maxfd + 1, &r_set, NULL, NULL, NULL); 这里maxfd+1的目的是为了能够处理maxfd,因为select的实现比较老了。
    3. select函数返回后,我们需要写一个状态机,去判断是sockfd还是其他fd也有。两种不同的fd,会有不同的IO操作。
  • 代码如下:

    fd_set rf_set, r_set;
    FD_ZERO(&rf_set);//清空集合
    FD_SET(sockfd, &rf_set);//增加sockfd到集合int maxfd = sockfd;//设置最大fd
    while (1) {r_set = rf_set;int ret_code = select(maxfd + 1, &r_set, NULL, NULL, NULL);if (FD_ISSET(sockfd, &r_set)) {//acceptstruct sockaddr_in client_addr;int len = sizeof(struct sockaddr);int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);FD_SET(client_fd, &rf_set);//增加新的fd到fd_set里maxfd = maxfd > client_fd ? maxfd : client_fd;//更新maxfd} else {for (int i = sockfd + 1; i < maxfd + 1; ++i) {if (FD_ISSET(i, &r_set)) {//receivechar buf[128] = {0};int recv_len = recv(i, buf, 128, 0);printf("accept len: %d", recv_len);if (recv_len == -1){perror("recv error");break;} else if (recv_len == 0) {FD_CLR(i, &rf_set);close(i);//记得close,不然会一直在close_wait状态。break;} else {printf("recv success\n");}//sendprintf("send");send(i, buf, recv_len, 0);printf("send success");}}}
    }
    

优缺点

  • 优点:相较于多线程实现法,进行了极大的优化,减少线程资源消耗。
  • 缺点:
    1. 函数参数太多了,一共有五个参数,中间三个参数分别为r_set、w_set、e_set分别对应不同的事件。
    2. 使用select会频繁的进行内存拷贝,每当处理完数据就会将fd_set拷贝到内核态,而每次有事件发生都会将fd_set拷贝到用户态,频繁拷贝,浪费很多资源,性能很低。
    3. select能够处理的最大连接数为1024个。

poll

poll写法和改进点

  • 改进点:

    1. 优化了函数参数,将中间三个参数变为一个,增加了一个结构体,用来存储监听哪些事件(读写)、是否监听以及fd。用户定义一个数组,作为传出参数,传给poll函数,等函数返回后,用户需要遍历数组,对于数组每个元素,如果revents被改变,那么就是发生的对应的事件,用户可以进行相应的操作。
    2. 因为用户可以自定义数组大小,所以poll解决了select只能监听1024个文件描述符的问题。
  • 缺点:

    • 底层的内核监听过程和select类似,也需要把数组从用户态拷贝到内核态,只是把修改fdset换成了每个数组元素中revents的值,其他相差不大。
  • 写法代码如下:

    // struct pollfd {
    //     int fd;			/* File descriptor to poll.  */
    //     short int events;		/* Types of events poller cares about.  */
    //     short int revents;		/* Types of events that actually occurred.  */
    // };
    struct pollfd fds[1024];
    fds[sockfd].fd = sockfd;
    fds[sockfd].events = POLLIN;
    int maxfd = sockfd;
    while (1) {int ret_code = poll(fds, maxfd + 1, -1);if (fds[sockfd].revents & POLLIN) {struct sockaddr_in client_addr;int len = sizeof(struct sockaddr);int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);fds[client_fd].fd = client_fd;fds[client_fd].events = POLLIN;maxfd = maxfd > client_fd ? maxfd : client_fd;} for (int i = sockfd + 1; i < maxfd + 1; ++i) {if (fds[i].revents & POLLIN) {//receivechar buf[128] = {0};int recv_len = recv(i, buf, 128, 0);printf("accept len: %d", recv_len);if (recv_len == -1){perror("recv error");break;} else if (recv_len == 0) {fds[i].events = 0;fds[i].fd = -1;close(i);break;} else {printf("recv success\n");}//sendprintf("send");send(i, buf, recv_len, 0);printf("send success");}}
    }
    

epoll(使用最多,重中之重)

epoll写法和改进点

  • 改进点:

    1. epoll是在内核中申请一块缓存,存放一个节点,里面包含一个红黑树和双向链表,采用红黑树存储要监听的fd,然后将监听到的fd存储到一个双向链表里,这样加大了内核的查询效率,以及用户拿到的是被触发的结果集,不需要再去遍历所有的fd了,也就是不需要用户自己维护maxfd了。
    2. 虽然epoll也定义了一个数组,但是epoll的写法采用的共享内存的方式,而select和poll是单纯的拷贝。
  • 缺点:

    • 当服务遇到大量的短连接时,就会频繁的调用epoll_ctl系统调用,消耗系统资源。
  • 写法步骤:

    1. 调用epoll_create系统调用会在内核为该进程创建一个句柄,其中包含红黑树根节点和一个双向链表分别存储需要监听的节点和准备就绪的fd。
    2. 调用epoll_ctl系统调用,向红黑树节点中添加、删除、修改想要操作的fd,起初肯定是sockfd,并且注册一个回调函数,告诉内核如果有准备就绪的节点就添加到双向链表中。
    3. 调用epoll_wait系统调用,由内核去检测双向链表是否为空,如果为空,就sleep,直到链表里有数据时,epoll_wait被唤醒,将数据返回给用户,当然用户也可以自己设置超时时间,在该时间到达后,即使没有数据也会返回告知用户。
  • 代码如下:

    /*struct epoll_event {uint32_t events;	epoll_data_t data;	} __EPOLL_PACKED;*/int epoll_fd = epoll_create(1);//这里只要大于0就可以,内部实现改为链表了。struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = sockfd;epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);struct epoll_event epoll_events[1024] = {};while (1) {int ret_code = epoll_wait(epoll_fd, epoll_events, 1024, -1);for (int i = 0; i < ret_code; ++i) {int connt_fd = epoll_events[i].data.fd;if (connt_fd == sockfd) {struct sockaddr_in client_addr;int len = sizeof(struct sockaddr);int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);ev.events = EPOLLIN;ev.data.fd = client_fd;epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);printf("clientfd: %d\n", client_fd);} else if (epoll_events[i].events & EPOLLIN){//receivechar buf[10] = {0};int recv_len = recv(connt_fd, buf, 10, 0);printf("accept len: %d", recv_len);if (recv_len == -1){perror("recv error");close(connt_fd);continue;} else if (recv_len == 0) {epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL);close(i);continue;} else {printf("recv success\n");}//sendprintf("send");send(connt_fd, buf, recv_len, 0);printf("send success");}}}
    

LT模式和ET模式

LT模式
  • 是什么?
    • 我们在处理数据的时候,会有一个存储数据的buffer数组,如果buffer数组的长度小于要读取的数据长度,那么recv函数只会读取buffer长度的数据,然后等下次epoll_wait返回后,再继续读取数据,但是上次的数据需要保存,不然就丢失了。
  • 应用场景:
    • 可以采用LT模式解决粘包问题,先读取数据的长度,然后循环把数据全部读取出来。
ET模式
  • 是什么?
    • 内核针对每一个fd只返回一次(也就是说针对每次事件,epoll_wait只返回一次),后续就不再对该fd进行返回,这就使得用户必须一次性读完,否则只能等到下次fd有事件触发的时候才能接着读取上次的数据。
  • 应用场景:
    • 当处理大文件的时候,一次性读取所有数据。因为如果采用LT模式的话,文件太大会针对这个fd频繁调用epoll_wait系统调用。
LT模式如何解决数据保存问题?
  • 直接看代码(定义一个全局变量,存储每个fd对应的buffer,并且记录上次读到的位置idx):
    struct fd_events    
    {int fd;char r_buffer[128];char w_buffer[128];int r_idx;int w_idx;//这里还可以添加事件以及回调函数。
    };//解决LT模式下无法存数据的问题,每次读完后拼接到buffer后,并修改idx到数组末尾
    struct fd_events all_events[1024] = {};
    

reactor模式

针对IO处理的两种写法

  • 针对IO状态机处理
    • 上面的epoll示例就是针对fd去处理的,对sockfd和其他fd分别处理。
  • 针对事件状态机划分
    • 因为epoll_event里的events是有不同的事件的,我们可以针对不同的事件进行状态机判断,然后分别调用不同的回调函数。
    • 代码如下:
         if (epoll_events[i].events & EPOLLIN) {//xxx} else if (epoll_events[i].events & EPOLLOUT) {//xxx} else if (epoll_events[i].events & EPOLLERR) {//xxx}
      
  • reactor模式就是针对不同事件调用不同的回调函数的模式。

思考题

main函数如何被执行的?

  • 程序启动时,操作系统会创建一个进程,并为该进程分配一块内存空间,然后将可执行文件加载到内存中,然后操作系统会进行一些初始化准备工作,之后找到程序执行入口点main函数的地址,将控制权交给main函数的代码逻辑,程序开始执行,程序执行结束后,将控制权返回给操作系统,操作系统根据退出码来判断程序的运行状态。

time_wait怎么产生的?如果有大量的time_wait是为什么?

  • 首先time_wait状态是先断开连接的一方会产生的状态,四次挥手时,当最后一次挥手发送完后,就进入了time_wait状态。
  • 如果有大量的time_wait的原因是什么?
    1. 目前感觉是因为大量的短连接,并且是服务端先主动调用close函数造成的。

close_wait怎么产生的?如果有大量的close_wait是为什么?

  • close_wait是在四次挥手中收到对方的断开连接请求,并且向对方发送ack确认后就进入了close_wait状态,是被断开一方产生的状态。
  • 如果有大量的close_wait是为什么?
    1. 忘记调用close函数,没有去发送finTCP包就会一直处于close_wait状态。
    2. 代码逻辑有问题,可能跳过了close的执行。

epoll里是否使用了mmap?

  • epoll使用的共享内存的方式实现,否则和select、poll一样还是需要进行大量的数据拷贝。

epoll是否是线程安全的?

  • 首先epoll本身是linux下对IO事件进行监听的机制,可以用来处理高并发连接,如果是多进程使用一个epoll_fd实例,那么需要一定的线程同步机制来保证数据一致性。如果是每个线程对应不同的实例,则是线程安全的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/781999.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

结构体类型,结构体变量的创建和初始化 以及结构中存在的内存对齐

一般结构体类型的声明 struct 结构体类型名 { member-list; //成员表列 }variable-list; //变量表列 例如描述⼀个学⽣&#xff1a; struct Stu { char name[20]; //名字 int age; //年龄 char sex[5]; //性别 }&#xff1b; //结构体变量的初始化 int main() { S…

牛客NC30 缺失的第一个正整数【simple map Java,Go,PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/50ec6a5b0e4e45348544348278cdcee5 核心 Map参考答案Java import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可…

Modelsim手动仿真实例

目录 1. 软件链接 2. 为什么要使用Modelsim 3. Modelsim仿真工程由几部分组成&#xff1f; 4. 上手实例 4.1. 新建文件夹 4.2. 指定目录 4.3. 新建工程 4.4. 新建设计文件&#xff08;Design Files&#xff09; 4.5. 新建测试平台文件&#xff08;Testbench Files&…

企业数据被新型.rmallox勒索病毒加密,应该如何还原?

.rmallox勒索病毒为什么难以解密&#xff1f; .rmallox勒索病毒难以解密的主要原因在于其采用了高强度的加密算法&#xff0c;并且这些算法被有效地实施在了病毒程序中。具体来说&#xff0c;.rmallox勒索病毒使用了RSA和AES这两种非常成熟的加密算法。RSA是一种非对称加密算法…

08、Lua 函数

Lua 函数 Lua 函数Lua函数主要有两种用途函数定义解析&#xff1a;optional_function_scopefunction_nameargument1, argument2, argument3..., argumentnfunction_bodyresult_params_comma_separated 范例 : 定义一个函数 max()Lua 中函数可以作为参数传递给函数多返回值Lua函…

Laravel 数据库:判断数据表是否存在

检测某个表是否存在&#xff1a; if (Schema::hasTable(table_name)) { // } 在某个表不存在的情况下再执行创建操作&#xff1a; if ( ! Schema::hasTable(table_name)) { // 创建数据库表的代码 } 如果你想安全的 drop 掉一个数据表&#xff0c;使用以下&#xf…

蓝桥杯刷题记录之蓝桥王国

只是记录 这题用迪杰斯特拉来就行&#xff0c;我写的是堆优化版本 import java.util.*;public class Main{static Scanner s new Scanner(System.in);static int n,m,startPoint1;static List<Edge>[] table;//邻接表,因为是稀疏图static long[] dist;static boolean[] …

Day25 代码随想录(1刷) 回溯

39. 组合总和 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 &#xff0c;并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限制重复…

3D汽车模型线上三维互动展示提供视觉盛宴

VR全景虚拟看车软件正在引领汽车展览行业迈向一个全新的时代&#xff0c;它不仅颠覆了传统展览的局限&#xff0c;还为参展者提供了前所未有的高效、便捷和互动体验。借助于尖端的vr虚拟现实技术、逼真的web3d开发、先进的云计算能力以及强大的大数据处理&#xff0c;这一在线展…

瑞吉外卖实战学习--6、通过try和catch进行异常处理

try和catch进行异常处理 效果图前言1、公共拦截器进行异常处理1.1、创建公共报错处理的方法1.2、@ControllerAdvice中设置要拦截的类1.3、@ExceptionHandler中写处理的异常类2、完善错误拦截器2.1、效果效果图 前言 当用户名重复数据库会报错,此时就需要捕获异常操作 1、公共…

Spring: 在SpringBoot项目中解决前端跨域问题

这里写目录标题 一、什么是跨域问题二、浏览器的同源策略三、SpringBoot项目中解决跨域问题的5种方式&#xff1a;使用CORS1、自定 web filter 实现跨域(全局跨域)2、重写 WebMvcConfigurer(全局跨域)3、 CorsFilter(全局跨域)4、使用CrossOrigin注解 (局部跨域) 一、什么是跨域…

社交网络的未来:Facebook如何塑造数字社交的下一章

引言 社交网络已成为我们生活中不可或缺的一部分&#xff0c;而Facebook作为其领军者&#xff0c;一直在塑造着数字社交的未来。本文将深入探讨Facebook在未来如何塑造数字社交的下一章&#xff0c;并对社交网络的发展趋势进行展望和分析。 1. 引领虚拟社交的潮流 Facebook将…

Lombok之@SneakyThrows

1前言&#xff1a; 这里记录一个SneakyThrows的用法&#xff0c;关于他的用法&#xff0c;在官网上可以知道的很清楚 官网介绍&#xff1a;http://projectlombok.org/features/SneakyThrows.html 2代码示例 个人理解&#xff1a;在代码中&#xff0c;使用 try&#xff0c;cat…

C++vector和C语言数组的区别

vector 是 C 标准模板库&#xff08;STL&#xff09;中的一个类模板&#xff0c;它提供了一个动态数组的功能&#xff0c;能够根据需要自动增长或缩小。而 C 语言数组则是 C 语言提供的一种固定大小的序列容器。下面是 vector 和 C 语言数组之间的一些主要区别&#xff1a; 动…

VRP and related algorithms for logistics distribution综述的笔记

选了些自己感兴趣的。 车辆路径问题(VRP)是当今物流公司面临的最关键挑战之一。自1959年丹齐格和兰姆泽(1959年)介绍了卡车调度问题以来,研究人员一直在研究车辆路由和交付调度。它被认为是车辆路径问题(VRP)的范例,并且涉及从中央仓库到地理分散的客户的货物配送。 自…

Polars学习-常用函数代码

下载包 导入包 数据读写 import polars as pl from datetime import datetimedf pl.DataFrame({"integer": [1, 2, 3],"date": [datetime(2022, 1, 1),datetime(2022, 1, 2),datetime(2022, 1, 3),],"float": [4.0, 5.0, 6.0],} ) print(df) …

小白从0学习ctf(web安全)

文章目录 前言一、baby lfi&#xff08;bugku-CTF&#xff09;1、简介2、解题思路1、解题前置知识点2、漏洞利用 二、baby lfi 2&#xff08;bugku-CTF&#xff09;1.解题思路1、漏洞利用 三、lfi&#xff08;bugku CTF&#xff09;1、解题思路1、漏洞利用 总结 前言 此文章是…

uniapp保留两位小数,整数后面加.00

直接把方法粘贴进去就能用 <text class"bold">总收入&#xffe5;{{formater(priceNumer)}}</text>export default {data() {priceNumer: 199.999, // 总收入},methods: {// 保留两位小数formater(data) {if(!data) return 0.00data parseFloat(data).…

SpringBoot -- 整合SpringMVC

SpringBoot已经替我们整合了许多框架并进行了默认的配置&#xff0c;我们只需要在依赖中导入spring-boot-starter-web&#xff0c;就可以直接使用SpringMVC以及web场景下的已经整合好的功能。但SpringBoot的默认配置可能无法满足我们所有的需求&#xff0c;那么我们怎么进行自定…

Java复习第十二天学习笔记(JDBC),附有道云笔记链接

【有道云笔记】十二 3.28 JDBC https://note.youdao.com/s/HsgmqRMw 一、JDBC简介 面向接口编程 在JDBC里面Java这个公司只是提供了一套接口Connection、Statement、ResultSet&#xff0c;每个数据库厂商实现了这套接口&#xff0c;例如MySql公司实现了&#xff1a;MySql驱动…