(1)bind 函数如何选择绑定地址:bind 函数的基本用法如下:
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); //
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
std::cout << "bind listen socket error." << std::endl;
return -1;
}
如上所示,INADDR_ANY是一个宏,相当于地址0.0.0.0,如果应用程序不关心bind绑定的ip地址,可以使用INADDR_ANY(如果是IPv6,则对应in6addr_any),这样底层的(协议栈)服务会自动选择
一个合适的ip地址,这样使在一个有多个网卡机器上选择ip地址问题变得简单。bind函数可以绑定多个可选的IP,如果指定本机访问,可设置127.0.0.1,如果是局域网内部访问,设置内网IP,如果是外
网访问,设置0.0.0.0或 INADDR_ANY。
(2)bind函数绑定端口号:如果将 bind 函数中的端口号设置成0,那么操作系统会随机给程序分配一个可用的侦听端口,windows的端口号是0~65535,实际可用的更少,此外,除了服务器可以调
用bind绑定指定端口外,客户端也可以调用以指定的端口号去连接服务器。另外,Linux 的 nc 命令有个 -p 选项(字母 p 是小写),这个选项的作用就是 nc 在模拟客户端程序时,可以使用指定端口
号连接到服务器程序上去,比如 命令nc -v -p 9999 127.0.0.1 3000 则指定9999区连接127.0.0.1 的3000端口。
(3)socket的阻塞和非阻塞模式:所谓阻塞模式,就当某个函数“执行成功的条件”当前不能满足时,该函数会阻塞当前执行线程,程序执行流在超时时间到达或“执行成功的条件”满足后恢复继续执
行。而非阻塞模式恰恰相反,即使某个函数的“执行成功的条件”不当前不能满足,该函数也不会阻塞当前执行线程,而是立即返回,继续运行执行程序流。无论是 Windows 还是 Linux 平台,默认创建
的 socket 都是阻塞模式的。
windows下使用ioctlsocket() 函数 将 socket 设置成非阻塞模式,ioctlsocket() 签名如下:
int ioctlsocket(SOCKET s,long cmd, u_long *argp); //cmd 参数设置为 FIONBIO,argp设置为0即可将 socket 设置成阻塞模式,而将argp 设置成非 0 即可设置成非阻塞模式
linux下有三种方式可将socket设置成 非阻塞模式:
1.使用 fcntl() 函数或 ioctl() 函数给创建的 socket 增加 O_NONBLOCK 标志来将 socket 设置成非阻塞模式:
int oldSocketFlag = fcntl(sockfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
fcntl(sockfd, F_SETFL, newSocketFlag);
2.socket() 创建函数时直接设置非阻塞,函数签名如:int socket(int domain,int type,int protocol);给参数type 参数增加一个 SOCK_NONBLOCK 标志即可,如下:
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
3.使用扩展函数 accept4(),直接将 accept 函数返回的 socket 设置成非阻塞的,函数签名:int accept4(int sockfd, struct sockaddr *addr,socklen_t* addrlen,int flags); 例如:
int clientfd = accept4(listenfd, &clientaddr, &addrlen, SOCK_NONBLOCK);
send 函数本质上并不是往网络上发送数据,而是将应用层发送缓冲区的数据拷贝到内核缓冲区中去,至于什么时候数据会从网卡缓冲区中真正地发到网络中去要根据 TCP/IP 协议栈的行为来确
定,这种行为涉及到一个叫 nagel 算法和 TCP_NODELAY 的 socket 选项。recv 函数本质上也并不是从网络上收取数据,而只是将内核缓冲区中的数据拷贝到应用程序的缓冲区中,当然拷贝完成以
后会将内核缓冲区中该部分数据移除。
当 socket 是阻塞模式的,继续调用 send/recv 函数会导致程序阻塞在 send/recv 调用处;当 socket 是非阻塞模式,继续调用 send/recv 函数,send/recv 函数不会阻塞程序执行流,而是会立即
出错返回-1,我们会得到一个相关的错误码,Linux 平台上该错误码为 EWOULDBLOCK 或 EAGAIN(这两个错误码值相同),Windows 平台上错误码为 WSAEWOULDBLOCK。
(4)nagle算法:nagle算法的是操作系统网络通信层的一种发送数据包机制,如果开启,则一次放入网卡缓冲区中的数据(利用send或write等)较小时,可能不会立即发出去,只要当多次send或者
write之后,网卡缓冲区中的数据足够多时,才会一次性被协议栈发送出去,操作系统利用这个算法减少网络通信次数,提高网络利用率。对于实时性要求比较高的应用来说,可以禁用nagle算法。这
样send或write的小数据包会立刻发出去。系统默认是开启的,禁用方法如下:
long noDelay = 1;
setsockopt(m_hSocket, IPPROTO_TCP, TCP_NODELAY,(LPSTR)&noDelay, sizeof(long)); //noDelay为1-禁用,0-启用
(5)linux epoll模型:Linux 从内核 2.6引入epoll模型,首先创建fd函数:
#include<sys/epoll.h>
int epoll_create(int size); //size 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值 ,返回-1表示创建失败
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event); // 绑定事件,返回-1表示失败,参数 epfd 即上文提到的 epollfd,参数 op表示操作类型,EPOLL_CTL_ADD(添加)、
EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除),当操作类型为NULL时,第四个参数 可以设置NULL,参数 fd,即需要被操作的 fd,参数 event,一个 epoll_event 结构体的地址,
epoll_event 结构体定义如下:
struct epoll_event
{
uint32_t events; /* 需要检测的 fd 事件,取值与 poll 函数一样 */
epoll_data_t data; /* 用户自定义数据, 本质上是一个 Union 对象,8个字节*/
};
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout); //检测事件 ,参数 events 是一个 epoll_event 结构数组的首地址,这是一个输出参数,函数调用成功后,events 中存放
的是与就绪事件相关 epoll_event 结构体数组;参数 maxevents 是数组元素的个数;timeout 是超时时间,单位是毫秒,如果设置为 0,epoll_wait 会立即返回。当 epoll_wait 调用成功会返回有事件
的 fd 数目;如果返回 0 表示超时;调用失败返回 -1。如下例子所示:
while (true)
{
epoll_event epoll_events[1024];
int n = epoll_wait(epollfd, epoll_events, 1024, 1000);
if (n < 0)
{
//被信号中断
if (errno == EINTR)
continue;
//出错,退出
break;
}
else if (n == 0)
{
//超时,继续
continue;
}
for (size_t i = 0; i < n; ++i)
{
// 处理可读事件
if (epoll_events[i].events & POLLIN)
{
}
// 处理可写事件
else if (epoll_events[i].events & POLLOUT)
{
}
//处理出错事件
else if (epoll_events[i].events & POLLERR)
{
}
}
}
epoll_wait 与 poll 的区别:epoll_wait 函数调用完之后,我们可以直接在 event 参数中拿到所有有事件就绪的 fd,直接处理即可(event 参数仅仅是个出参);而 poll 函数的事件集合调用前后
数量都未改变,只不过调用前我们通过 pollfd 结构体的 events 字段设置待检测事件,调用后我们需要通过 pollfd 结构体的 revents 字段去检测就绪的事件( 参数 fds 既是入参也是出参)。
与 poll 的事件宏相比,epoll 新增了一个事件宏EPOLLET,这就是所谓的边缘触发模式(EdgeTrigger,ET),而默认的模式我们称为水平触发模式(LevelTrigger,LT)。这两种模式的区别在于:
对于水平触发模式,一个事件只要有,就会一直触发;(读取数据时可选择读取字节数目)
对于边缘触发模式,只有一个事件从无到有才会触发。(因此读取数据时要一次性全部读完,循环调用 recv 函数直到 recv 出错,错误码是EWOULDBLOCK(EAGAIN 一样))