一.插口缓存(套接字缓存)
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;
}