from:
https://blog.csdn.net/analogous_love/article/category/7257412
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 地址可以选择。首先,这台机器对外访问的ip地址是 120.55.94.78,这台机器在当前局域网的地址是 192.168.1.104;同时这台机器有本地回环地址127.0.0.1。
如果只想让本机 访问 bind 函数中的地址 可以使用127.0.0.1 ; 若服务器只想被局域网内部的机器访问, 地址可以使用 192.168.1.104; 若希望被公网访问 那么就可以使用 0.0.0.0 or INADDR_ANY 当年部署到阿里云的时候遇到这个坑人的细节 解决了 好久 才搞定的。
上面是 bind函数在serv 端口的调用情况 问题分析
如果在客户端 调用 bind 函数 会出现 上面问题呢?
发现三个 client 进程使用的端口号仍然是系统随机分配的,也就是说绑定 0 号端口和没有绑定效果是一样的。
情形三:客户端绑定一个固定端口
我们这里使用 20000 端口,当然读者可以根据自己的喜好选择,只要保证所选择的端口号当前没有被其他程序占用即可,服务器代码保持不变,客户端绑定代码中的端口号从 0 改成 20000。
发现 client 进程确实使用 20000 号端口连接到 server 进程上去了。这个时候如果我们再开启一个 client 进程,我们猜想由于端口号 20000 已经被占用,新启动的 client 会由于调用 bind 函数出错而退出
select api使用
Linux 平台下的 select 函数
select 函数的作用是检测一组 socket 中某个或某几个是否有“事件”就绪,这里的“事件”一般分为如下三类:
读事件就绪:
1 socket 内核中,接收缓冲区中的字节数大于等于低水位标记 SO_RCVLOWAT,此时调用 recv 或 read 函数可以无阻塞的读该文件描述符, 并且返回值大于0;
2 TCP 连接的对端关闭连接,此时调用 recv 或 read 函数对该 socket 读,则返回 0;
3 侦听 socket 上有新的连接请求;
4 socket 上有未处理的错误。
写事件就绪:
1 socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩) 大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写, 并且返回值大于0;
3 socket 的写操作被关闭(调用了 close 或者 shutdown 函数) ( 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号);
4 socket 使⽤非阻塞 connect 连接成功或失败之后;
异常事件就绪
socket 上收到带外数据。
=================================================================================
select 使用 流程
注意一些细节问题 就是
select 函数调用前后会修改 readfds、writefds 和 exceptfds 这三个集合中的内容(如果有的话),所以如果您想下次调用 select 复用这个变量,记得在下次调用前再次调用 select 前先使用 FD_ZERO 将集合清零,然后调用 FD_SET 将需要检测事件的 fd 再次添加进去。 (这里也是 select的三大缺点之一吧 相对于 epoll 模型 当然咯最主要的 不是这里 缺点 是在 while循环中的for循环检测 ,还有一个就是 内核和用户态进行 copy)
/**
* select函数示例,server端, select_server.cpp
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <sys/time.h>
#include <vector>
#include <errno.h>
//自定义代表无效fd的值
#define INVALID_FD -1
int main(int argc, char* argv[])
{
//创建一个侦听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1)
{
std::cout << "create listen socket error." << std::endl;
return -1;
}
//初始化服务器地址
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;
close(listenfd);
return -1;
}
//启动侦听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}
//存储客户端socket的数组
std::vector<int> clientfds;
int maxfd = listenfd;
while (true)
{
fd_set readset;
FD_ZERO(&readset);
//将侦听socket加入到待检测的可读事件中去
FD_SET(listenfd, &readset);
//将客户端fd加入到待检测的可读事件中去
int clientfdslength = clientfds.size();
for (int i = 0; i < clientfdslength; ++i)
{
if (clientfds[i] != INVALID_FD)
{
FD_SET(clientfds[i], &readset);
}
}
timeval tm;
tm.tv_sec = 1;
tm.tv_usec = 0;
//暂且只检测可读事件,不检测可写和异常事件
int ret = select(maxfd + 1, &readset, NULL, NULL, &tm);//细节就是 每次select调用结束后 可读 可写 异常 事件集合都需要从新设置 包括第5个超时参数 都要从新设置
if (ret == -1)
{
//出错,退出程序。
if (errno != EINTR)
break;
}
else if (ret == 0)
{
//select 函数超时,下次继续
continue;
}
else
{
//检测到某个socket有事件
if (FD_ISSET(listenfd, &readset))
{
//侦听socket的可读事件,则表明有新的连接到来
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//4. 接受客户端连接
int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (clientfd == -1)
{
//接受连接出错,退出程序
break;
}
//只接受连接,不调用recv收取任何数据
std:: cout << "accept a client connection, fd: " << clientfd << std::endl;
clientfds.push_back(clientfd);
//记录一下最新的最大fd值,以便作为下一轮循环中select的第一个参数
if (clientfd > maxfd)
maxfd = clientfd;
}
else
{
//假设对端发来的数据长度不超过63个字符
char recvbuf[64];
int clientfdslength = clientfds.size();
for (int i = 0; i < clientfdslength; ++i)//这里也是其效率低下的原因 相对于 epoll模型来说
{
if (clientfds[i] != -1 && FD_ISSET(clientfds[i], &readset))
{
memset(recvbuf, 0, sizeof(recvbuf));
//非侦听socket,则接收数据
int length = recv(clientfds[i], recvbuf, 64, 0);
if (length <= 0 && errno != EINTR)
{
//收取数据出错了
std::cout << "recv data error, clientfd: " << clientfds[i] << std::endl;
close(clientfds[i]);
//不直接删除该元素,将该位置的元素置位-1
clientfds[i] = INVALID_FD;
continue;
}
std::cout << "clientfd: " << clientfds[i] << ", recv data: " << recvbuf << std::endl;
}
}
}
}
}
//关闭所有客户端socket
int clientfdslength = clientfds.size();
for (int i = 0; i < clientfdslength; ++i)
{
if (clientfds[i] != INVALID_FD)
{
close(clientfds[i]);
}
}
//关闭侦听socket
close(listenfd);
return 0;
}
上面的demo 对select调用 第五个超时参数的设置需要注意一点细节就是 传入参数为 3种情况
1.非0值的 就是检测 等待到相对应的时间就返回
2.传入为NULL 参数设置为 NULL,则 select 函数会一直阻塞下去,直到我们需要的事件触发
3. 传入为0的 监测对应的检测事件 检测结束就返回 并不等待固定时间
对上面的总结 select 使用的4个注意事项
Linux select 函数的缺点也是显而易见的:
1 每次调用 select 函数,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 较多时会很大,
2 同时每次调用 select 函数都需要在内核遍历传递进来的所有 fd,这个开销在 fd 较多时也很大;
3 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义然后重新编译内核的方式提升这一限制,这样非常麻烦而且效率低下;
4 select 函数在每次调用之前都要对传入参数进行重新设定,这样做比较麻烦而且会降低性能。