TCP/IP实现(九) 插口I/O

本文深入解析了套接字缓存的工作原理,包括插口缓存的结构和管理,写与读系统调用的实现细节,以及带外数据与紧急模式的处理方式。特别关注了TCP和UDP在不同情况下的数据处理机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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; // 指向用于存储控制信息的buf
    msg.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_RECVDSTADDR
        if(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;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值