网络编程api bind函数细节 select 细节

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 函数在每次调用之前都要对传入参数进行重新设定,这样做比较麻烦而且会降低性能

 

网络通信基础重难点解析 05 :socket 的阻塞模式和非阻塞模式

 

posted on 2019-03-31 04:30  zhangkele  阅读(420)  评论(0编辑  收藏  举报

导航