基于C语言实现内存型数据库(kv存储)

打印 上一主题 下一主题

主题 892|帖子 892|积分 2676

基于C语言实现内存型数据库(kv存储)


  
   

  • 源代码仓库见Github:kv-store仓库
  • 共同我的讲解视频更佳:【C语言项目笔记】基于C语言实现内存型数据库(kv存储)。
  • 参考视频:“零声教育”的“linux基础架构-Kv存储”。
  • 其他源码:协程。
  
1. 项目背景

1.1 Redis先容

  本项目主要想仿照Redis的交互方式,实现一个基本的“内存型数据库”,以是起首来先容一下Redis。随着互联网的遍及,只要是上网的APP基本上都需要和相应的服务器请求数据,通常来说,这些数据被服务器保存在“磁盘”上的文件中,称之为“磁盘型数据库”。但是面对海量用户时(比如秒杀运动),磁盘IO的读写速率不够快从而导致用户体验下降,并且服务器数据库的压力也非常大。鉴于很多请求只是读取数据,这就启发我们将一些热门数据存放在内存中,以便快速响应请求、并且减轻磁盘的读写压力。
当然,上述只是一个初步的想法,后续如何清算内存数据、分布式存储等可以参考B站的科普视频,讲的非常简洁易懂:
   

  • 【趣话Redis第一弹】我是Redis,MySQL大哥被我坑惨了!—“缓存穿透、缓存击穿、缓存雪崩”、“定时删除、惰性删除、内存淘汰”
  • 【趣话Redis第二弹】Redis数据持久化AOF和RDB原理一次搞懂!—“RDB+AOF”
  • 【趣话Redis第三弹】Redis的高可用是怎么实现的?哨兵是什么原理?—“主观下线、客观下线”、“哨兵选举”、“故障转移”
  • 趣话Redis:Redis集群是如何工作的?—“哈希桶”、“集群工作+主从复制”
  下面是一些典范的口试题:
   

  • 为什么要利用Redis?
     

  • 从高并发上来说:直接操纵缓存能够承受的请求是远远大于直接访问数据库的,以是我们可以考虑把数据库中的部分数据转移到缓存中去。这样用户的一部分请求会直接到缓存,而不用颠末数据库。
  • 从高性能上来说:用户第一次访问数据库中的某些数据,因为是从硬盘上读取的,以是这个过程会比力慢。将该用户访问的数据存在缓存中,下一次再访问这些数据的时候就可以直接从缓存中获取了。操纵缓存就是直接操纵内存,以是速度相当快。
   

  • 为什么要利用Redis而不是其他的,比方Java自带的map或者guava?
       缓存分为本地缓存和分布式缓存,像map或者guava就是本地缓存。本地缓存最主要的特点是轻量以及快速,生命周期随着jvm的烧毁而结束。在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。
   

  • Redis应用场景有哪些?
     

  • 缓存热门数据,缓解数据库的压力。
  • 利用Redis原子性的自增操纵,可以实现计数器的功能。比如统计用户点赞数、用户访问数等。
  • 分布式锁。在分布式场景下,无法利用单机情况下的锁来对多个节点上的进程进行同步。可以利用Redis自带的SETNX命令实现分布式锁,除此之外,还可以利用官方提供的RedLock分布式锁实现。
  • 简单的消息队列。可以利用Redis自身的发布/订阅模式或者List来实现简单的消息队列,实现异步操纵。
  • 限速器。可用于限定某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不须要的压力。
  • 好友关系。利用集合的一些命令,比如交集、并集、差集等,实现共同好友、共同爱好之类的功能。
   

  • 为什么Redis这么快?
     

  • Redis是基于内存进行数据操纵的Redis利用内存存储,没有磁盘IO上的开销,数据存在内存中,读写速度快。
  • 接纳IO多路复用技能。Redis利用单线程来轮询形貌符,将数据库的操纵都转换成了变乱,不在网络I/O上浪费过多的时间。
  • 高效的数据布局。Redis每种数据类型底层都做了优化,目的就是为了追求更快的速度。
   

  • 参考视频:为什么要利用Redis?、Redis的应用场景有哪些?、Redis,好快!
  1.2 项目预期及基本架构

         图1 项目框架    于是我们现在就来实现这个“内存型数据库”,本项目利用C语言,默认键值对key-value都是char*类型。如上图所示,我们渴望“客户端”可以和“服务端”通讯,发送相应的指令并得到相应的信息。比如“客户端”插入一个新的键值对“(name: humu)”,那么就发送“SET name humu”;“服务端”接收到这个数据包后,执行相应的操纵,再返回“OK”给“客户端”。鉴于kv存储需要强查找的数据布局,我们可以利用rbtree、btree、b+tree、hash、dhash、array(数据量不多,比如http头)、skiplist、list(性能低不考虑)。终极,下表列出了我们要实现的所有数据布局及其对应的指令:
   表1 kv存储协议对应的数据布局及命令    操纵/
数据布局插入查找删除计数存在arraySET key valueGET keyDELETE keyCOUNTEXIST keyrbtreeRSET key valueRGET keyRDELETE keyRCOUNTREXIST keybtreeBSET key valueBGET keyBDELETE keyBCOUNTBEXIST keyhashHSET key valueHGET keyHDELETE keyHCOUNTHEXIST keydhashDHSET key valueDHGET keyDHDELETE keyDHCOUNTDHEXIST keyskiplistZSET key valueZGET keyZDELETE keyZCOUNTZEXIST key备注返回OK/Fail,
表示插入键值对是否成功返回对应的value返回OK/Fail,
删除对应的键值对返回当前数据布局中
存储的键值对数量返回True/False,
判断是否存在对应的键值对    进一步,由于网络编程中的“Hello,World!程序”就是实现一个echo,收到什么数据就原封不动的发送回去。以是我们渴望在此基础上,将kv存储写成一个独立的进程,和网络收发相干代码隔脱离,进而提升代码的可维护性。另外在网络协议的选择中,由于我们的键值对设置通常较短只有十几个字符(比如set key value),而http协议的协议头就有几十个字符,有效数据占比太低;udp协议只能在底层网卡确认对方收到,但没法在应用层确认,以是不可控;于是我们网络通信协议选择tcp。于是对于“服务端”,我们就可以有如下的架构设计:
   
    图2 “服务端”程序架构   

  • 网络层:负责收发数据。本项目中都是“字符串”。
  • 协议层:将“网络层”传输过来的字符串进行拆解,若为无效指令直接返回相应的提示信息;若为有效指令则通报给“引擎层”进行进一步的处理,根据“引擎层”的处理结果给出相应的返回信息。
  • 引擎层:分为6种存储引擎,每种存储引擎都可以进行具体的增、删、查等操纵,也就是实现上表给出的5种命令。
  • 存储层:注意“内存型数据库”的数据在内存中,但若后续需要“持久化”也会将数据备份到磁盘中。
  2. 服务端原理及代码框架

2.1 网络数据回环的实现

  在利用原生的socket库函数进行网络通信时,会一直阻塞等待客户端的连接/通信请求,这个线程就做不了其他的事情,非常浪费资源。于是“reactor模式”应运而生,也被称为“基于变乱驱动”,核心点在于:注册变乱、监听变乱、处理变乱。也就是说,线程找了一个“秘书”专门负责去监听网络端口是否有“网络通信”的发生,线程就可以去做其他的事情;等到线程想处理“网络通信”的时候一起全部通知给该线程,然后这个“秘书”继续监听。显然,有这样一个“秘书”存在,可以将“网络通信”、“业务处理”分隔开,一个线程可以同时处理多个客户端的请求/通信,也就实现了“IO多路复用一个线程”。下面是常见的三种reactor模式:
   

  • reactor单线程模型:只分配一个线程。显然若线程的“业务处理”时间过长,会导致“秘书”积压的变乱过多,甚至可能会丢弃一些变乱。本模型不适合计算密集型场景,只适合业务处理非常快的场景(本项目就是业务处理非常快)。
  • reactor多线程模型:分配一个主线程和若干子线程。主线程只负责处理“网络通信”,“业务处理”则交给子线程处理。本模式的好处是可以充实利用多核CPU性能,但是带来了线程安全的问题。并且只有一个线程响应“网络通信”,在瞬时高并发的场景下容易成为性能瓶颈。
  • 主从reactor多线程模型:在上述多线程模型的基础上,再额外开辟出新的子线程专门负责“与客户端通信”,主线程则只负责“连接请求”。
  参考B站视频:【Java口试】先容一下Reactor模式
  下面利用epoll作为“秘书”,接纳“reactor单线程模型”,完成网路数据回环(echo),也就是“服务端”程序框架的“网络层”:
main.c–共356行
  1. /*
  2. * zv开头的变量是zvnet异步网络库(epoll)。
  3. * kv开头的变量是kv存储协议解析。
  4. */
  5. #include<stdio.h>
  6. #include<stdlib.h>
  7. #include<string.h>
  8. #include<errno.h>
  9. #include<unistd.h>
  10. #include<sys/socket.h>
  11. #include<netinet/in.h>
  12. #include<fcntl.h>
  13. #include<sys/epoll.h>
  14. #include"kvstore.h"
  15. /*-------------------------------------------------------*/
  16. /*-----------------------异步网路库-----------------------*/
  17. /*-------------------------------------------------------*/
  18. /*-----------------------函数声明-------------------------*/
  19. #define max_buffer_len      1024    // 读写buffer长度
  20. #define epoll_events_size   1024    // epoll就绪集合大小
  21. #define connblock_size      1024    // 单个连接块存储的连接数量
  22. #define listen_port_count   1       // 监听端口总数
  23. // 有如下参数列表和返回之类型的函数都称为 CALLBACK
  24. // 回调函数,方便在特定epoll事件发生时执行相应的操作
  25. typedef int (*ZV_CALLBACK)(int fd, int events, void *arg);
  26. // 回调函数:建立连接
  27. int accept_cb(int fd, int event, void* arg);
  28. // 回调函数:接收数据
  29. int recv_cb(int clientfd, int event, void* arg);
  30. // 回调函数:发送数据
  31. int send_cb(int clientfd, int event, void* arg);
  32. // 单个连接
  33. typedef struct zv_connect_s{
  34.     // 本连接的客户端fd
  35.     int fd;
  36.     // 本连接的读写buffer
  37.     char rbuffer[max_buffer_len];
  38.     size_t rcount;  // 读buffer的实际大小
  39.     char wbuffer[max_buffer_len];
  40.     size_t wcount;  // 写buffer的实际大小
  41.     size_t next_len;  // 下一次读数据长度(读取多个包会用到)
  42.     // 本连接的回调函数--accept()/recv()/send()
  43.     ZV_CALLBACK cb;
  44. }zv_connect;
  45. // 连接块的头
  46. typedef struct zv_connblock_s{
  47.     struct zv_connect_s *block;  // 指向的当前块,注意大小为 connblock_size
  48.     struct zv_connblock_s *next;  // 指向的下一个连接块的头
  49. }zv_connblock;
  50. // 反应堆结构体
  51. typedef struct zv_reactor_s{
  52.     int epfd;   // epoll文件描述符
  53.     // struct epoll_event events[epoll_events_size];  // 就绪事件集合
  54.     struct zv_connblock_s *blockheader;  // 连接块的第一个头
  55.     int blkcnt;  // 现有的连接块的总数
  56. }zv_reactor;
  57. // reactor初始化
  58. int init_reactor(zv_reactor *reactor);
  59. // reator销毁
  60. void destory_reactor(zv_reactor* reactor);
  61. // 服务端初始化:将端口设置为listen状态
  62. int init_sever(int port);
  63. // 将本地的listenfd添加进epoll
  64. int set_listener(zv_reactor *reactor, int listenfd, ZV_CALLBACK cb);
  65. // 创建一个新的连接块(尾插法)
  66. int zv_create_connblock(zv_reactor* reactor);
  67. // 根据fd从连接块中找到连接所在的位置
  68. // 逻辑:整除找到所在的连接块、取余找到在连接块的位置
  69. zv_connect* zv_connect_idx(zv_reactor* reactor, int fd);
  70. // 运行kv存储协议
  71. int kv_run_while(int argc, char *argv[]);
  72. /*-------------------------------------------------------*/
  73. /*-----------------------函数定义-------------------------*/
  74. // reactor初始化
  75. int init_reactor(zv_reactor *reactor){
  76.     if(reactor == NULL) return -1;
  77.     // 初始化参数
  78.     memset(reactor, 0, sizeof(zv_reactor));
  79.     reactor->epfd = epoll_create(1);
  80.     if(reactor->epfd <= 0){
  81.         printf("init reactor->epfd error: %s\n", strerror(errno));
  82.         return -1;
  83.     }
  84.     // 为链表集合分配内存
  85.     reactor->blockheader = (zv_connblock*)calloc(1, sizeof(zv_connblock));
  86.     if(reactor->blockheader == NULL) return -1;
  87.     reactor->blockheader->next = NULL;
  88.     // 为链表集合中的第一个块分配内存
  89.     reactor->blockheader->block = (zv_connect*)calloc(connblock_size, sizeof(zv_connect));
  90.     if(reactor->blockheader->block == NULL) return -1;
  91.     reactor->blkcnt = 1;
  92.     return 0;
  93. }
  94. // reator销毁
  95. void destory_reactor(zv_reactor* reactor){
  96.     if(reactor){
  97.         close(reactor->epfd);  // 关闭epoll
  98.         zv_connblock* curblk = reactor->blockheader;
  99.         zv_connblock* nextblk = reactor->blockheader;
  100.         do{
  101.             curblk = nextblk;
  102.             nextblk = curblk->next;
  103.             if(curblk->block) free(curblk->block);
  104.             if(curblk) free(curblk);
  105.         }while(nextblk != NULL);
  106.     }
  107. }
  108. // 服务端初始化:将端口设置为listen状态
  109. int init_sever(int port){
  110.     // 创建服务端
  111.     int sockfd = socket(AF_INET, SOCK_STREAM, 0);  // io
  112.     // fcntl(sockfd, F_SETFL, O_NONBLOCK);  // 非阻塞
  113.     // 设置网络地址和端口
  114.     struct sockaddr_in servaddr;
  115.     memset(&servaddr, 0, sizeof(struct sockaddr_in));
  116.     servaddr.sin_family = AF_INET;  // IPv4
  117.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 0.0.0.0,任何地址都可以连接本服务器
  118.     servaddr.sin_port = htons(port);  // 端口
  119.     // 将套接字绑定到一个具体的本地地址和端口
  120.     if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))){
  121.         printf("bind failed: %s", strerror(errno));
  122.         return -1;
  123.     }
  124.     // 将端口设置为listen(并不会阻塞程序执行)
  125.     listen(sockfd, 10);  // 等待连接队列的最大长度为10
  126.     printf("listen port: %d, sockfd: %d\n", port, sockfd);
  127.     return sockfd;
  128. }
  129. // 将本地的listenfd添加进epoll
  130. int set_listener(zv_reactor *reactor, int listenfd, ZV_CALLBACK cb){
  131.     if(!reactor || !reactor->blockheader) return -1;
  132.     // 将服务端放进连接块
  133.     reactor->blockheader->block[listenfd].fd = listenfd;
  134.     reactor->blockheader->block[listenfd].cb = cb;  // listenfd的回调函数应该是accept()
  135.     // 将服务端添加进epoll事件
  136.     struct epoll_event ev;
  137.     ev.data.fd = listenfd;
  138.     ev.events = EPOLLIN;
  139.     epoll_ctl(reactor->epfd, EPOLL_CTL_ADD, listenfd, &ev);
  140.     return 0;
  141. }
  142. // 创建一个新的连接块(尾插法)
  143. int zv_create_connblock(zv_reactor* reactor){
  144.     if(!reactor) return -1;
  145.     // 初始化新的连接块
  146.     zv_connblock* newblk = (zv_connblock*)calloc(1, sizeof(zv_connblock));
  147.     if(newblk == NULL) return -1;
  148.     newblk->block = (zv_connect*)calloc(connblock_size, sizeof(zv_connect));
  149.     if(newblk->block == NULL) return -1;
  150.     newblk->next = NULL;
  151.     // 找到最后一个连接块
  152.     zv_connblock* endblk = reactor->blockheader;
  153.     while(endblk->next != NULL){
  154.         endblk = endblk->next;
  155.     }
  156.     // 添加上新的连接块
  157.     endblk->next = newblk;
  158.     reactor->blkcnt++;
  159.     return 0;
  160. }
  161. // 根据fd从连接块中找到连接所在的位置
  162. // 逻辑:整除找到所在的连接块、取余找到在连接块的位置
  163. zv_connect* zv_connect_idx(zv_reactor* reactor, int fd){
  164.     if(!reactor) return NULL;
  165.     // 计算fd应该在的连接块
  166.     int blkidx = fd / connblock_size;
  167.     while(blkidx >= reactor->blkcnt){
  168.         zv_create_connblock(reactor);
  169.         // printf("create a new connblk!\n");
  170.     }
  171.     // 找到这个连接块
  172.     zv_connblock* blk = reactor->blockheader;
  173.     int i = 0;
  174.     while(++i < blkidx){
  175.         blk = blk->next;
  176.     }
  177.     return &blk->block[fd % connblock_size];
  178. }
  179. // 回调函数:建立连接
  180. // fd:服务端监听端口listenfd
  181. // event:没用到,但是回调函数的常用格式
  182. // arg:应该是reactor*
  183. int accept_cb(int fd, int event, void* arg){
  184.     // 与客户端建立连接
  185.     struct sockaddr_in clientaddr;  // 连接到本服务器的客户端信息
  186.     socklen_t len_sockaddr = sizeof(clientaddr);
  187.     int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len_sockaddr);
  188.     if(clientfd < 0){
  189.         printf("accept() error: %s\n", strerror(errno));
  190.         return -1;
  191.     }
  192.     // 将连接添加进连接块
  193.     zv_reactor* reactor = (zv_reactor*)arg;
  194.     zv_connect* conn = zv_connect_idx(reactor, clientfd);
  195.     conn->fd = clientfd;
  196.     conn->cb = recv_cb;
  197.     conn->next_len = max_buffer_len;
  198.     conn->rcount = 0;
  199.     conn->wcount = 0;
  200.     // 将客户端添加进epoll事件
  201.     struct epoll_event ev;
  202.     ev.data.fd = clientfd;
  203.     ev.events = EPOLLIN;  // 默认水平触发(有数据就触发)
  204.     epoll_ctl(reactor->epfd, EPOLL_CTL_ADD, clientfd, &ev);
  205.     printf("connect success! sockfd:%d, clientfd:%d\n", fd, clientfd);
  206. }
  207. // 回调函数:接收数据
  208. int recv_cb(int clientfd, int event, void* arg){
  209.     zv_reactor* reactor = (zv_reactor*)arg;
  210.     zv_connect* conn = zv_connect_idx(reactor, clientfd);
  211.     int recv_len = recv(clientfd, conn->rbuffer+conn->rcount, conn->next_len, 0);  // 由于当前fd可读所以没有阻塞
  212.     if(recv_len < 0){
  213.         printf("recv() error: %s\n", strerror(errno));
  214.         close(clientfd);
  215.         // return -1;
  216.         exit(0);
  217.     }else if(recv_len == 0){
  218.         // 重置对应的连接块
  219.         conn->fd = -1;
  220.         conn->rcount = 0;
  221.         conn->wcount = 0;
  222.         // 从epoll监听事件中移除
  223.         epoll_ctl(reactor->epfd, EPOLL_CTL_DEL, clientfd, NULL);
  224.         // 关闭连接
  225.         close(clientfd);
  226.         printf("close clientfd:%d\n", clientfd);
  227.     }else if(recv_len > 0){
  228.         conn->rcount += recv_len;
  229.         // conn->next_len = *(short*)conn->rbuffer;  // 从tcp协议头中获取数据长度,假设前两位是长度
  230.         
  231.         // 处理接收到的字符串,并将需要发回的信息存储在缓冲区中
  232.         printf("recv clientfd:%d, len:%d, mess: %s\n", clientfd, recv_len, conn->rbuffer);
  233.         // conn->rcount = kv_protocal(conn->rbuffer, max_buffer_len);
  234.         // 将kv存储的回复消息(rbuffer)拷贝给wbuffer
  235.         // printf("msg:%s len:%ld\n", msg, strlen(msg));
  236.         memset(conn->wbuffer, '\0', max_buffer_len);
  237.         memcpy(conn->wbuffer, conn->rbuffer, conn->rcount);
  238.         conn->wcount = conn->rcount;
  239.         memset(conn->rbuffer, 0, max_buffer_len);
  240.         conn->rcount = 0;
  241.         // 将本连接更改为epoll写事件
  242.         conn->cb = send_cb;
  243.         struct epoll_event ev;
  244.         ev.data.fd = clientfd;
  245.         ev.events = EPOLLOUT;
  246.         epoll_ctl(reactor->epfd, EPOLL_CTL_MOD, clientfd, &ev);
  247.     }
  248.     return 0;
  249. }
  250. // 回调函数:发送数据
  251. int send_cb(int clientfd, int event, void* arg){
  252.     zv_reactor* reactor = (zv_reactor*)arg;
  253.     zv_connect* conn = zv_connect_idx(reactor, clientfd);
  254.     int send_len = send(clientfd, conn->wbuffer, conn->wcount, 0);
  255.      if(send_len < 0){
  256.         printf("send() error: %s\n", strerror(errno));
  257.         close(clientfd);
  258.         return -1;
  259.      }
  260.     memset(conn->wbuffer, 0, conn->next_len);
  261.     conn->wcount -= send_len;
  262.     // 发送完成后将本连接再次更改为读事件
  263.     conn->cb = recv_cb;
  264.     struct epoll_event ev;
  265.     ev.data.fd = clientfd;
  266.     ev.events = EPOLLIN;
  267.     epoll_ctl(reactor->epfd, EPOLL_CTL_MOD, clientfd, &ev);
  268.     return 0;
  269. }
  270. // 运行kv存储协议
  271. int kv_run_while(int argc, char *argv[]){
  272.     // 创建管理连接的反应堆
  273.     // zv_reactor reactor;
  274.     zv_reactor *reactor = (zv_reactor*)malloc(sizeof(zv_reactor));
  275.     init_reactor(reactor);
  276.     // 服务端初始化
  277.     int start_port = atoi(argv[1]);
  278.     for(int i=0; i<listen_port_count; i++){
  279.         int sockfd = init_sever(start_port+i);
  280.         set_listener(reactor, sockfd, accept_cb);  // 将sockfd添加进epoll
  281.     }
  282.     printf("init finish, listening connet...\n");
  283.     // 开始监听事件
  284.     struct epoll_event events[epoll_events_size] = {0};  // 就绪事件集合
  285.     while(1){
  286.         // 等待事件发生
  287.         int nready = epoll_wait(reactor->epfd, events, epoll_events_size, -1);  // -1等待/0不等待/正整数则为等待时长
  288.         if(nready == -1){
  289.             printf("epoll_wait error: %s\n", strerror(errno));
  290.             break;
  291.         }else if(nready == 0){
  292.             continue;
  293.         }else if(nready > 0){
  294.             // printf("process %d epoll events...\n", nready);
  295.             // 处理所有的就绪事件
  296.             int i = 0;
  297.             for(i=0; i<nready; i++){
  298.                 int connfd = events[i].data.fd;
  299.                 zv_connect* conn = zv_connect_idx(reactor, connfd);
  300.                 // 回调函数和下面的的逻辑实现了数据回环
  301.                 if(EPOLLIN & events[i].events){
  302.                     conn->cb(connfd, events[i].events, reactor);
  303.                 }
  304.                 if(EPOLLOUT & events[i].events){
  305.                     conn->cb(connfd, events[i].events, reactor);
  306.                 }
  307.             }
  308.         }
  309.     }
  310.     destory_reactor(reactor);
  311.     return 0;
  312. }
  313. int main(int argc, char *argv[]){
  314.     if(argc != 2){
  315.         printf("please enter port! e.x. 9999.\n");
  316.         return -1;
  317.     }
  318.     // 初始化存储引擎
  319.     // kv_engine_init();
  320.     // 运行kv存储
  321.     kv_run_while(argc, argv);
  322.    
  323.     // 销毁存储引擎
  324.     // kv_engine_desy();
  325.     return 0;
  326. }
  327. /*-------------------------------------------------------*/
复制代码
  注:只有251行、346行、352行是后续和kv存储有关的接口函数。可以发现“网络层”和“协议层”被很好的隔脱离来。
   
    图3 验证网络数据回环  可以看到上述实现了网络数据回环的功能。并且进一步来说,假如客户端利用浏览器(http协议)对该端口进行访问,那么对接收到的数据包进行http协议拆包,根据其请求的内容返回相应的信息(如html文件),那么就是我们所熟知的“web服务器”了。为什么是“烂大街”啊,一代比一代卷是吧

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

圆咕噜咕噜

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表