一.插口缓存(套接字缓存)

struct   sockbuf {u_long sb_cc;      // 缓存中的数据大小u_long   sb_hiwat;   /* max actual char count */u_long   sb_mbcnt;   // 缓存mbuf的数量u_long  sb_mbmax;   /* max chars of mbufs to use */long sb_lowat;   /* low water mark */struct  mbuf *sb_mb;    // 缓存链表struct   selinfo sb_sel; // 用于记录用select监听该插口的进程short sb_flags;   // 缓存的一些状态标志,比如该缓存是否已上锁,是否有进程在等待上锁等等short sb_timeo;   // 用于限制一个进程读写套接字缓存时的超时时限,默认为0,即无限等待// 可以使用setsockopt函数通过SO_SNDTIMEO和SO_RCVTIMEO选项进行修改
} so_rcv, so_snd;

注意,进程访问套接字缓存时是加锁的,因此多个进程访问套接字缓存是安全的。

二.写系统调用

写系统调用有write,writev,send,sendto,sendmsg,所有的这些系统调用都会都会直接或间接调用sosend函数,该函数会将进程传来的数据复制到内核,并传给与插口相关的协议。且前四个系统调用都可以用sendmsg函数来进行替换(但是只有前两个函数调用可以作用于其它描述符,后三个只能用于接口描述符),因此,此处只对sendmsg函数进行说明。

1.sendMsg的实现概述

sendmsg会间接调用sosend函数将数据交付给相应的协议层,当绝不将数据直接添加到套接字缓存中,因为这是数据层该做的工作,比如UDP就不会将数据放入缓存。sosend函数首先会sblock函数获取socket发送缓存的锁,接着根据协议类型来进行不同的交付方式,对于有边界的报文协议(如UDP),必须等到有足够的缓存时,一次性拷贝到内核的存储空间mbuf中,再交付给协议层,否则(如TCP),每次交付一部分数据(一个mbuf)至协议层(只有当套接字缓存可用空间高于低水位时才交付,否则等待套接字缓存空闲)。当若设置了非阻塞模式,当空间不够时,立刻返回EWOULDBLOCK(即EAGAIN,请求资源不足)。对于边界报文的协议而言,若一次性通过writev写入的数据过大(查过了套接字结构中的高水位sb_hiwat),则也立刻返回EMSGSIZE,因为对于数据报协议而言,调用一次writev就是发送一个数据报。

另外,sosend函数会首先检查套接字是否被禁止(已关闭写so->so_state & SS_CANTSENDMORE为真),若是则返回EPIPE并向所属进程发送SIGPIPE信号(该信号的默认行为是中止进程,muduo对该信号做了忽略处理)。接着检查套接字是否已连接,若不是则返回ENOTCONN。对于无连接协议若未指定目的地址则返回EDESTADDRREQ。

三.读系统调用

1.recvmsg函数的使用

recvmsg函数的第二个参数比较复杂,在次对其进行讲解,并对控制信息参数的使用进行举例说明:

下面举一个获取UDP数据报首部目的地址的例子(一般用于获取广播报文):.h在前,.cpp在后

#include <sys/socket.h>
#include <string>
#include <arpa/inet.h>
#include <iostream>class UDP_Server
{
public:UDP_Server(std::string ip, int port);void recvData();private:int _socket;int _port;std::string _ip;sockaddr_in _servAddr;};

.cpp:

#include <string.h>
#include <netinet/in.h>
#include <sys/param.h>typedef union { // 注意这是一个联合体,便于重新解读这块内存cmsghdr msghdr;  // cmsghdr的首部结构// 用于存储控制信息的缓冲区char control[CMSG_SPACE(sizeof(in_pktinfo))]; // CMSG_SPACE宏的作用:sizeof(in_pktinfo) + sizeof(cmsghdr);
} cmsg_un;UDP_Server::UDP_Server(std::string ip, int port) : _port(port),_ip(ip)
{_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);int on = 1;setsockopt(_socket, IPPROTO_IP, IP_PKTINFO, &on,sizeof(int));setsockopt(_socket, SOL_SOCKET, SO_BROADCAST, &on, sizeof(int)); // 设置套接字为允许接收广播报文bzero(&_servAddr, sizeof(sockaddr_in));if(_ip != "") {inet_pton(AF_INET, _ip.c_str(), &_servAddr.sin_addr);}else {_servAddr.sin_addr.s_addr = htonl(INADDR_ANY);}_servAddr.sin_port = htons(_port);if(bind(_socket, (sockaddr*)&_servAddr, sizeof(sockaddr)) < 0) {std::cout<< "bind error: " << errno <<std::endl;if(errno == EADDRNOTAVAIL) {std::cout<< "EADDRNOTAVAIL" <<std::endl;}}
}void UDP_Server::recvData()
{char recvBuf[65536];msghdr msg;iovec iov;cmsg_un ctrlMsg;cmsghdr *cmsgptr;in_pktinfo *pi;int namelen =sizeof(sockaddr_in);iov.iov_base = recvBuf;iov.iov_len = sizeof(recvBuf);msg.msg_name = NULL;msg.msg_namelen =   0;msg.msg_control = &ctrlMsg.msghdr; // 指向用于存储控制信息的bufmsg.msg_controllen = sizeof(cmsg_un);msg.msg_iov = &iov;msg.msg_iovlen = 1;msg.msg_flags = 0;int n = 0;if( (n = recvmsg(_socket, &msg, 0)) < 0) {std::cout<< "recv error: " << errno <<std::endl;}recvBuf[n] = '\0';std::cout<<"recv date: "<<recvBuf<<std::endl;// CMSG_FIRSTHDR宏用于获取缓冲中第一个cmsghdr结构,CMSG_NXTHDR用于指向下一个for(cmsgptr = CMSG_FIRSTHDR(&msg); cmsgptr; cmsgptr = CMSG_NXTHDR(&msg,cmsgptr)) {// 注意在测试的centOS下,获取目的地址的类型是IP_PKTINFO,而非IP_RECVDSTADDRif(cmsgptr->cmsg_level == IPPROTO_IP && cmsgptr->cmsg_type == IP_PKTINFO) {// CMSG_DATA返回指向数据部分的指针pi = (in_pktinfo*)CMSG_DATA(cmsgptr);sockaddr_in lAddr;memcpy(&lAddr.sin_addr, &pi->ipi_addr, sizeof(in_addr));inet_ntop(AF_INET, &lAddr.sin_addr, recvBuf, sizeof(recvBuf));std::cout<<"des ip: "<<recvBuf<<std::endl;}//break;}//if()
}

2.带外数据的读取实现

当要读取带外数据时,插口层只是为带外数据分配一块额外的缓存,并向相关协议进行起码请求。因此关于TCP带外数据的读取实现到讨论TCP协议时进行说明

四.带外数据与紧急模式

带外数据即OOB数据,一般用于通知重要事件,其拥有比普通数据更高的优先级。许多运输层确实提供了真正的带外数据:使用同一个连接的独立的逻辑数据通道作为正常的数据通道。但TCP没有真正的带外数据,而应该称之为紧急模式。UDP无任何带外数据。

1.TCP紧急模式

TCP紧急模式中对用于描述紧急数据的字段很少,只有首部中URG比特与一个16bit的紧急指针被置为一个正的偏移量。URG比特用于通知对端,紧急模式已启动。当套接字发送缓存中存在一个OOB数据时,便会将下一个发送分节的TCP首部的URG标志置位,但OOB数据不一定随下一分节发出,当接收端收到URG后便会进入紧急模式,直至越过紧急数据。而16bit紧急指针是一个偏移量,用于计算紧急字段的最后一个字节的序号(紧急指针 + 首部的32位序号),紧急数据字节号(urgSeq)=TCP报文序号(seq)+紧急指针(urgpoint)−1。

当紧急指针所指数据到达TCP接收端时,该字节数据即可能被拉出带外,也可能被留在带内,即在线留存。当设置了SO_OOBINLINE套接字选项时(默认情况下该选项是禁止的),会将该字节数据留在套接字缓存中,否则将被放至该连接的一个独立的单字节带外缓冲区。

2.几种读取紧急数据的方式

1)读取放在套接字缓存中的紧急数据

当将紧急数据放在带内时,即套接字缓存中时,只能通过sockatmark或者ioclt函数先检查带外标记,插口层确保当一个套接字缓存中存在紧急数据时,进行一次读系统调用最多只会读到紧急数据字节之前的数据,这是可用通过调用sockatmark或ioctl函数来检查带外数据标记,若返回真,则说明之后一个数据是紧急数据。代码如下:

void OOBSErver::recvOOBInline()
{int n = 0, on = 1;pollfd listenFds[10];char recvBuf[65536];listenFds[0].fd = _connFd;listenFds[0].events = POLLRDNORM | POLLPRI;setsockopt(_connFd, SOL_SOCKET, SO_OOBINLINE, &on, sizeof(on)); // 设置为将紧急数据放在带内while(1) {int activeNum = ::poll(listenFds, 2, 10000); // 相较于UNP中的数据在此处添加了poll,这是需要的,原因在下面给出if(sockatmark(_connFd)) { // 检查带外标记std::cout<< "at OOB mark"<<std::endl;}if( (n = read(_connFd, recvBuf, sizeof(recvBuf) - 1)) == 0 ) {std::cout<<errno;std::cout<<"recv FIN"<<std::endl;exit(0);}recvBuf[n] = 0;std::cout << "read " <<n<<" bytes date: " << recvBuf << std::endl;}
}

在该段程序中添加poll的原因是存在以下这种情况:前一次read调用将套接字缓存的数据多空,接下来调用sockatmark进行判断带外标记为假,之后准备调用read,但这时到达了一条带外标记,则不会打印出“at OOB mark”,进程也无法得知这是一条什么数据。

而且经测试发现,若发送端连续多次调用send(,,,MSG_OOB)发送带外数据,接收端并未将旧对带外数据丢弃,而是将旧的紧急数据做为普通数据读入。若将紧急数据留在带外单字节缓存中则不会出现该问题,此时若旧的紧急数据未读又来了新的则会将旧的丢弃。因此不推荐使用SO_OOBINLINE选项。

2)使用poll函数关注高优先级数据POLLPRI

可以使用poll关注某个套接字上的POLLPRI(高优先级数据可读事件)和POLLRDNORM(普通数据可读事件),当某个套接字可读时根据返回的事件类型进行判断是普通数据(POLLRDNORM)还是紧急数据(POLLPRI),代码如下:

void OOBSErver::usePollRecv()
{pollfd listenFds[10];int n = 0;char recvBuf[65536];listenFds[0].fd = _connFd;listenFds[0].events = POLLRDNORM | POLLPRI;while(1) {int activeNum = ::poll(listenFds, 2, 10000);if(listenFds[0].revents > 0) {if(listenFds[0].revents & POLLPRI) { // 高优先级数据可读// 若紧急数据放在带外单字节缓存,需要通过参数MSG_OOB进行读取n = recv(_connFd, recvBuf, sizeof(recvBuf), MSG_OOB); if(n < 0) {if(errno == EINVAL) {std::cout << "EINVAL"<<std::endl; // 带外数据尚未到达continue;}}recvBuf[n] = 0;std::cout << "read " <<n<<" bytes OOB date: " << recvBuf << std::endl;}if(listenFds[0].revents & POLLRDNORM) { // 普通数据可读n = recv(_connFd, recvBuf, sizeof(recvBuf), 0);if(n == 0) {std::cout<<"recv FIN"<<std::endl;exit(0);}recvBuf[n] = 0;std::cout << "read " <<n<<" bytes normal date: " << recvBuf << std::endl;}}}
}

3)使用select

通过select等待普通数据(将套接字描述符添加到集合ret,即可读集合)或带外数据(将描述符添加到xset,即异常集合)。由于select是水平触发,因此当进程进入紧急状态后,便会一直触发异常,直到进程读入越过带外数据。UNP中给出了一种解决办法,即只在读入普通数据后才select异常条件。这样是可行的,因为若前一次没读到带外数据则说明带外数据还未到,带外数据之前肯定有普通数据(否则第一个含URG的报文就应该含有OOB数据),则应先读普通数据,读后再尝试关注带外数据。

void OOBSErver::useSelectRecv()
{fd_set rset, xset;int n = 0;bool justreadoob = false;char recvBuf[65536];while(1) {FD_SET(_connFd, &rset); // 添加至读集合if(!justreadoob)FD_SET(_connFd, &xset); // 添加至写集合select(_connFd + 1, &rset, NULL, &xset, NULL);if(FD_ISSET(_connFd, &xset)) {n = recv(_connFd, recvBuf, sizeof(recvBuf), MSG_OOB);justreadoob = true;FD_CLR(_connFd, &xset);if(n < 0) {if(errno == EINVAL) {std::cout << "EINVAL"<<std::endl;continue;}}recvBuf[n] = 0;std::cout << "read " <<n<<" bytes OOB date: " << recvBuf << std::endl;}if(FD_ISSET(_connFd, &rset)) {n = recv(_connFd, recvBuf, sizeof(recvBuf), 0);if(n == 0) {std::cout<<"recv FIN"<<std::endl;exit(0);}recvBuf[n] = 0;std::cout << "read " <<n<<" bytes normal date: " << recvBuf << std::endl;justreadoob = false;}}
}

4)使用SIGURG信号

主要代码如下:

typedef std::function<void()> SigCallBack;
SigCallBack sigurgCb;void sig_urg(int signo) // 关联到信号的函数
{static int num = 0;std::cout << "recv URG: "<<++num<<std::endl;sigurgCb();  // 回调handerSIGURG函数
}void OOBSErver::useSigUrgRecv()
{sigurgCb = std::bind(&OOBSErver::handerSIGURG, this);fcntl(_connFd, F_SETOWN, getpid());signal(SIGURG, sig_urg); // 关联信号的处理函数
}void OOBSErver::handerSIGURG() // 真正处理SIGURG信号
{int n = 0;char recvBuf[65536];n = recv(_connFd, recvBuf, sizeof(recvBuf), MSG_OOB);recvBuf[n] = 0;std::cout << "read " <<n<<" bytes OOB date: " << recvBuf << std::endl;
}

TCP/IP实现(九) 插口I/O相关推荐

  1. TCP/IP详解--第十九章

     第19章 TCP的交互数据流 19.1    引言   前一章我们介绍了 TCP连接的建立与释放,现在来介绍使用 TCP进行数据传输的有关问 题. 一些有关 TCP通信量的研究如[Cacereset ...

  2. TCP/IP详解卷2之插口层

    TCP/IP详解卷2之插口层篇 插口层概述 主要功能 splnet处理 socket结构 系统调用 进程.描述符和插口 socket系统调用 socreate函数 getsock和sockargs函数 ...

  3. QT从入门到入土(九)——TCP/IP网络通信(以及文件传输)

    引言 TCP/IP通信(即SOCKET通信)是通过网线将服务器Server端和客户机Client端进行连接,在遵循ISO/OSI模型的四层层级构架的基础上通过TCP/IP协议建立的通讯.控制器可以设置 ...

  4. 《TCP/IP详解卷一》读书笔记九:IGMP和MLD

    由于工作需要和知识储备,重新系统地学习网络方面的知识,先从<TCP/IP详解卷一>开始,对看书的大体内容进行简单整理,在这里进行记录.记录只是对知识的整理过程,并不追求面面俱到. 概念 I ...

  5. 《图解TCP/IP》读书笔记九:网络安全

    9.2 网络安全构成要素 9.2.1 防火墙 组织机构(域)内部的网络与互联网相连时,为了避免域内受到非法访问的威胁,往往会设置防火墙(使用NAT(NAPT)的情况下,由于限定了可以从外部访问的地址, ...

  6. 【TCP/IP学习笔记1】 C语言讲解

    TCP/IP学习笔记(一) 一. TCP/IP结构:      TCP/IP是一个四层协议,结构如下:      1.应用层:各种应用程序和协议,如Http.FTP等.      2.传输层:TCP和 ...

  7. TCP/IP学习笔记(一)(转载)

    一.TCP/IP结构:      TCP/IP是一个四层协议,结构如下:      1.应用层:各种应用程序和协议,如Http.FTP等.      2.传输层:TCP和UDP      TCP提供一 ...

  8. 万字长文,一文搞懂TCP/IP和HTTP、HTTPS

    来自:非科班的科班 TCP/IP概念 TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现 ...

  9. iOS中 HTTP/Socket/TCP/IP通信协议详解 韩俊强的博客

    版权声明:本文为博主原创文章,未经博主允许不得转载. 每日更新关注:http://weibo.com/hanjunqiang  新浪微博 简单介绍: [objc] view plaincopy //  ...

  10. TCP/IP状态详解

    今天犯懒了,本来自己也做了一些相应的笔记,但是发现这篇写的更好一些,简单易懂,而且有图有真相,为了方便以后查看,在此转载了,在此基础上加了自己的笔记                 TCP正常建立和关 ...

最新文章

  1. 《OpenGL ES 3.x游戏开发(下卷)》一1.2 顶点数组对象
  2. linux 移动目录树到子目录中,Linux系统管理员工具包: 移动Linux/UNIX目录
  3. Azkaban-two_server模式-安装3和启动运行
  4. 华大单片机HC32L136J8TA读取DS18B20温度(源码+时钟配置)
  5. IDEA安装“Translation”插件
  6. java面向对象测试题二_JAVA面向对象-测试题
  7. [每日一题] OCP1z0-047 :2013-07-12 多表插入
  8. 本计算机无法加入家庭组,win10系统无法加入家庭组是怎么回事?
  9. 史上最浅显易懂的Git教程3 分支管理
  10. window10运行python弹出商店_Python上架Windows 10应用商店,但主要用于学习,正式项目还...
  11. 百亿独角兽爱学习教育集团:如何在半个月内搭建一套完整的课堂互动系统?...
  12. VS提示error C2011: “timespec”:“struct”类型重定义
  13. 基于PHP图书馆图书借阅管理系统
  14. 震旦打印机打不开首选项
  15. SWOT分析思维的一些基本思考与见解
  16. kindle看pdf乱码,Kindle中文乱码问题解决办法
  17. Echarts地图深入+散点
  18. python中seaborn报错These `style` levels are missing dashes解决办法
  19. 盗取手机验证码诈骗的克星来了:号码认证服务为你保驾护航
  20. R语言 | 将CSV文件中原本为空白值的chr数据赋值为NA

热门文章

  1. gazebo如何加载sdf文件的模型
  2. vue项目中使用rem替换px-使用方法-01
  3. 【医学图像处理】 2 灰度直方图、图像二值化(阈值分割)
  4. 解决集群中MATLAB无法启动并行池的问题
  5. 摄像头寻找斑马线上拐点和摄像头图像压缩
  6. 被996拖垮的年轻人:“干嘛离职,你可以离婚啊!”
  7. Macfee 删除办法
  8. 为什么深度学习(Deep Learning)要使用GPU而不是CPU?
  9. Xcode支持iOS6、iOS7版本
  10. 大数据时代企业,所需要的技术有哪些?