网络编程

poll函数及简单服务器客户端编程

当需要同时监听多个文件描述符时,就需要I/O复用函数,I/O复用函数有select、poll、epoll,今天主要使用poll函数。
poll()接受一个指向结构’struct pollfd’列表的指针,其中包括了你想测试的文件描述符和事件。事件由一个在结构中事件域的比特掩码确定。当前的结构在调用后将被填写并在事件发生后返回。
函数原型:

#include<poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd{
    int fd;         /*file descriptor*/
    short events;   /*requested events*/
    short revents;  /*returned events*/
}

函数参数:fds是要监听的fd的数组,nfds是数组个数,timeout 超时时间 -1是阻塞;

函数说明:通过传入的events的类型去判断返回的类型是否一致,如果一致就该干事了。
events:
poll参考链接

epoll函数

参考链接

epoll的使用

用到的数据结构:

typedef union epoll_data {
	void ptr;
	int fd;
	__uint32_t u32;
	__uint64_t u64;
} epoll_data_t;
 
struct epoll_event {
	__uint32_t events;    / Epoll events /
	epoll_data_t data;    / User data variable /
};

函数

1、epoll_create函数
    函数声明:int epoll_create(int size)
    该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。在linux-2.4.32内核中根据size大小初始化哈希表的大小,在linux2.6.10内核中该参数无用,使用红黑树管理所有的文件描述符,而不是hash。
2、epoll_ctl函数
    函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
    该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
    参数:epfd:由 epoll_create 生成的epoll专用的文件描述符;
                op:要进行的操作例如注册事件,可能的取值
EPOLL_CTL_ADD 注册、
EPOLL_CTL_MOD 修改、
EPOLL_CTL_DEL 删除
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功返回0,不成功返回-1
3、epoll_wait函数
函数声明:int epoll_wait(int epfd,struct epoll_event   events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生;
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(ms);-1永不超时,直到有事件产生才触发,0立即返回。
返回发生事件数。-1有错误。

更多详细例子,可以参考腾讯后台开发那本书,以及链接
好的介绍
epoll

使用epoll好处

Epoll是Linux内核为处理大批量文件描述符而做了改进的poll,是Linux下多路复用I/O接口的增强版本,支持水平触发和边缘触发两种方式。相对于select等I/O复用方式,它具有支持大数目的描述符,I/O效率不随注册的描述符数目增加而线性下降(传统的select以及poll的效率会因为注册描述符数量的线形递增而导致呈二次乃至三次方的下降),和使用mmap加速内核与用户空间的消息传递等优点。

select函数简单说明

select函数
在编程的过程中,经常会遇到许多阻塞的函数,好像read和网络编程时使用的recv, recvfrom函数都是阻塞的函数,当函数不能成功执行的时候,程序就会一直阻塞在这里,无法执行下面的代码。这时就需要用到非阻塞的编程方式,使用select函数就可以实现非阻塞编程。
select函数是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。
使用 select 函数时,最关键的地方是如何动态维护 select()的 3 个参数 readfds 、 writefds
和 exceptfds 。 **作为输入参数, readfds 应该标记所有的需要检测的“可读事件”的句柄,其中
永远包括那个检测 connect()的那个“母”句柄;**同时, writefds 和 exceptfds 应该标记所有需
要检测的“可写事件”和“错误事件” 的句柄(使用 FD_SET()标记) 。
select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。详细的原理请看select原理

int select(int nfds,  fd_set* readset,  fd_set* writeset,  fe_set* exceptset,  struct timeval* timeout);

参数:

   nfds           需要检查的文件描述字个数
   readset     用来检查可读性的一组文件描述字。
   writeset     用来检查可写性的一组文件描述字。
   exceptset  用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
   timeout      超时,填NULL为阻塞,填0为非阻塞,其他为一段超时时间

返回值:

   返回fd的总数,错误时返回SOCKET_ERROR
int select (fd_set rnaxfdp, fd_set *readfds, fd_set *wri tefds, fd_set *errorfds, struct
timeval*time out) ;

这里面用到了两个结构体:fd_set 和 timeval 。结构体fd_set可以理解为一个集合,这个
集合中存放的是文件描述符( file descriptor ),即文件句柄,这可以认为是常说的普通意义的
文件;当然 UNIX 下任何设备、管道、 FIFO 等都是文件形式,所以毫无疑问,一个 socket
就是一个文件, socket 句柄就是一个文件描述符 。 “ set 集合可以通过 一些宏由 人为来操作,
比如以下代码:

fd_set set ;
FD_ZERO(&set) ; /*将 set 清零*/
FD_SET(fd , &set) ; / *将 fd 加入 set */
FD_CLR(fd, &set) ; /*将 fd 从 set 中清除*/
FD_工 SSET(fd, &set) ; / *如果 fd 在 set 中则真

结构体 timeval 是一个常用的结构,用来代表时间值,有两个成员,一个是秒数,另一
个是毫秒数 。
接着讲下 se lect 的各个参数所表示的含义,如下所述 。
( 1 ) maxfdp 是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最
大值加 1

( 2) readfds 是指向 fd_set 结构的指针,这个集合中应该包括文件描述符 。 因为要监视
文件描述符的读变化的,即关心是否可以从这些文件中读取数据,如果这个集合中有一个文
件可读, select 就会返回 一个大于 0 的值,表示有文件可读 。 如果没有可读的文件,则根据
timeout 参数再判断是否超时**:若超出 timeout 的时间, select 返回 O ;若发生错误返回负值;**
也可以传入 NULL 值,表示不关心任何文件的读变化 。
( 3 ) writefds 是指向“ set 结构的指针,这个集合中应该包括文件描述符 。 因为要监视
文件描述符的 写 变化的,即关心是否可以向这些文件中写入数据,如果这个集合中有一个文
件可写, select 就会返回 一个大于 0 的值,表示有文件可写 。 如果没有可写的文件,则根据
timeout 参数再判断是否超时:若超出 timeout 的时间, select 返回 O ;若发生错误返回负值;
也可以传入 NULL 值,表示不关心任何文件的写变化 。
( 4 ) errorfds 同上面两个参数的意图,用来监视文件错误异常 。
( 5) timeout 是 select 的超时时间 , 这个参数至关重要,它可以使 se lect 处 于 3 种状态:
1若将 NULL 以形参 传入,即不传入时间结构,就是将 se lect 置于阻塞状态, 一 定等到监
视文件描述符集合中某个文件描述符发 生变化为止;
2若将时间值设为 0 ,就变成一个纯粹的非阻塞 函数,不 管 文件描述柯:是再有变化,都立刻返回继续执行,文件无变 化返回 0 ,有
变化返回 一个正值;
3 timeout 的值大于 0 ,这就是等待的超时时间,即 select 在 timeout 时
间内阻塞 ,超时时间之内有 事 件到来就返回了,否则在超时后不管怎样一 定返回,返回值
同上述 。
( 6 )返回值:准备就绪的描述符数,若超时则返回 0 ,若出错则返回- 1 。

fd_set介绍

fd_set
fd_set简短介绍

粘包问题

TCP 是个“流”协议,所谓流,就是没有界限的一串数据 。 大家可以将其想象河里的
流水,是连成一片的,其间是没有分界线的 。 但一般通信程序开发是需要定义一个个相五独
立的数据包的,比如用于登录的数据包、用于注销的数据包等 。 **由于 TCP “流”的特性以及
网络状况,在进行数据传输时假设我们连续调用两次 send 分别发送两段数据 datal 和 data2
在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况) 。
( 1 )先接收到 datal ,然后接收到 data2 。
( 2 )先接收到 datal 的部分数据,然后接收到 datal 余下的部分以及 data2 的全部 。
( 3 )先接收到了 datal 的全部数据和 data2 的部分数据,然后接收到了 data2 的余下的
数据 。
(4 ) 一次性接收到了 datal 和 data2 的全部数据 。
对于( 1 )这种情况正是我们需要的,不再做讨论 。 对于( 2 )、 ( 3 )和( 4 )的情况就是
常说的“粘包”,就需要把接收到的数据进行拆包,拆成一个个独立的数据包;而为了拆包
就必须在发送端进行封包 。
**对于 UDP 来说就不存在拆包的问题,因为 UDP 是个”数据包”协议,也就是两段数据
间是有界限的,在接收端要么接收不到数据要么就是接收一段完整 的数据,不会少接 收也不
会多接收 。
为什么会出现( 2 )、( 3 )和( 4 )的情况呢,有以下几点原因 。
1 )由 Nagle 算法造成的发送端的粘包 。 前面有提到 Nagle 算法是一种改善 网络传输效率
的算法,但也可能造成困扰 。 简单来说,当要提交一段数据给 TCP 发送时, TCP 并不立刻发
送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次
把多段数据发送出去 。 像( 3 )和( 4 )的情况就有可能是 Nagle 算法造成的 。
2 )接收端接收不及时造成的接收端粘包 。 TCP 会把接收到的数据存在自己的缓 冲区中,
然后通知应用层取数据 。 当应用层由于某些原因不能及时取出 TCP 的数据,就会造成 TCP
缓冲区中存放了多段数据 。
“粘包”可发生在发送端也可发生在接收端 。
最初遇到”粘包”的问题时,大家可能觉得可以在两次 send 之间调用 sleep 来休眠一小
段时间,以此来解决 。 这个解决方法的缺点是显而易见的:使传输效率大大降低,而且也并
不可靠 。 对数据包进行封包和拆包,就能解决这个问题 。
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后
讲过滤非法包时会加上“包尾”内容) 。 包头其实上是个大小固定的结构体,其中有个结构
体成员变量表示包体的长度,这是个很重要的变量 ,其他的结构体成员可根据需要自己定
义 。 根据固定的包头长度以及包头中含有的包体长度的变量值就能正确的拆分出 一个完整的
数据包。

利用底层的缓冲区来进行拆包时,由于 TCP 也维护了 一个缓 冲区,所以可以利用 TCP
的缓冲区来缓存发送的数据,这样一来就不需要为每一个连接分配一个缓冲区了,对于利用
缓冲区来拆包,也就是循环不停地接收包头给出的数据,直到收够为止,这就是一个突整的
TCP 包 。

为了解决“粘包”的问题,大家通常会在所发送的内容前加上发送内容的长度,所以
对方就会先收 4 Byte ,解析获得接下来需要接收的长度,再进行收包 。

accept函数,在三次握手之后取得连接

一个 socket 可以 accept 多次 。 实际上 socket 的设计者可能特意
为多客户端的情况留下了伏笔,让 accept()能够返回 一个新的 socket 。 下面是 accept 接口的
原型 :

int accept(int fd, struct sockaddr *addr , socklen_t *addr l en) ;

输入参数臼是从 socket()、 bind()和 listen()中沿用下来的 socket 句柄值 。 执行完 bind()
和 listen ()后,操作系统已经开始在指定的端口处监昕所有的连接请求,如果有请求,则将
该连接请求加入请求队列 。 调用 accept()接口正是从 socket fd 的请求队列抽取第 一个连接
信息,创建一个与 “同类的新的 socket 返回句柄,这个新的 socket 句柄即是后续 read ()和
recv()的输入参数 。 如果请求队列当前没有请求,则 accept()将进入阻塞状态直到有请求进
入队列 。

服务器调用listen进行监听
客户端调用connect来发送syn报文
服务器协议栈负责三次握手的交互过程。连接建立后,往listen队列中添加一个成功的连接,直到队列的最大长度。
服务器调用accept从listen队列中取出一条成功的tcp连接,listen队列中的连接个数就少一个

客户端发送SYN给服务器
服务器发送SYN+ACK给客户端
客户端发送ACK给服务器
连接建立,调用accept()函数获取连接,三次握手跟accept调用没半毛钱关系

io模型 -阻塞io 非阻塞io 区别联系; 同步io 异步io区别

参考腾讯后台书 7.1节

  • 阻塞io 非阻塞io 区别联系; 同步io 异步io区别
    调用阻塞 IO 会 一直阻塞住对应的进程直到操作完成,而非阻塞 IO在内核还在准备数
    据的情况下会立刻返回 。

    **同步异步io区别:**两者的区别就在于同步 IO 进行 IO 操作时会阻塞进程 。 按照这个
    定义,之前所述的阻塞 IO 、非阻塞 IO 及多路 IO 复用都属于同步 IO 。 实际上,真实的 IO
    操作,就是例子中的 recvfrom 这个系统调用 。
    非阻塞 IO 在执行 recvfrom 这个系统调用的
    时候,如果内核的数据没有准备好,这时候不会阻塞进程 。 但是当内核中数据准备好时,
    recvfrom 会将数据从内核拷贝到用户内存中,这个时候进程则被阻塞 。 而异步 IO 则不一样,
    当进程发起 IO 操作之后,就直接返回,直到内核发送一个信号,告诉进程 IO 已完成,则在
    这整个过程中,进程完全没有被阻塞。

一个好的博客:五种io模型

IO复用

一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力。
其实多路复用的实现有多种方式:select、poll、epoll

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值