Linux socket网络小结

一个客户端,服务端只监听一个端口

客户端伪代码

int clt_socket = socket();//创建socket
connect(clt_socket , , );//连接客户端socket,端口和ip
while(1)//某种情况退出
{
	read();
	write();
}
close();//关闭socket

服务端伪代码

int server_socket = socket();//创建socket
bind(server_socket ,);//绑定服务端socket,端口和ip
listen(server_socket);//监听server_socket
int new_socket = accept();//获取连接的客户端socket
while(1)//某种情况退出
{
	read();
	write();
}
close();//关闭socket

总结:

  1. 客户端的三次握手发生在connect,服务端三次握手发生在accept。
  2. connect和accept都是阻塞函数。
  3. 阻塞函数的意思是,当前线程被阻塞,这个线程的其他业务都无法展开。
  4. 此场景太过简单,没有什么应用意义。
  5. listen维护一个客户端连接队列。accpet函数和这个队列中的客户端挨个建立连接

多个(但是不是很多)客户端,服务端只监听一个端口

客户端还是用上边的代码,但是启动3个客户端连接服务端。
服务端的listen可以监听到3个连接,并且维护一个队列。

服务端如果不修改:
因为程序是从上往下执行,虽然listen可以监听到3个连接,但是accept函数只运行一次,所以只对第一个连接的客户端传输数据。

所以对服务器做如下修改

int server_socket = socket();//创建socket
bind(server_socket ,);//绑定服务端socket,端口和ip
listen(server_socket);//监听server_socket
while(1)//某种情况退出
{
	int new_socket = accept();//获取连接的客户端socket
	while(1)//某种情况退出
	{
		read();
		write();
	}
	close();//关闭socket
}

好处:现在可以对3个客户端连接都传输数据。
问题:挨个地传输数据,让后边的客户端等很久。

解决方法:为每个客户端启一个线程
再对服务端进行修改:

int server_socket = socket();//创建socket
bind(server_socket ,);//绑定服务端socket,端口和ip
listen(server_socket);//监听server_socket
while(1)//某种情况退出
{
	int new_socket = accept();//获取连接的客户端socket
	new pthread(new_socket);//线程函数中传入与客户端通信的socket
}
//假如线程函数是new_socket_pthread
void new_socket_pthread(Socket skt)
{
	while(1)//某种情况退出
	{
		read();
		write();
	}
	close();//关闭socket
}

**这个时候会有4个server_socket,一个是服务端的socket,三个是和客户端通信的new_socket **

好处: 3个客户端可以并行进行了
问题:每个客户端启动一个线程浪费资源,可以在线程中维护一个客户端队列,一个线程处理多个客户端socket。

解决办法https://www.cnblogs.com/qigaohua/p/5688998.html

list<Socket > l_socket;
int server_socket = socket();//创建socket
bind(server_socket ,);//绑定服务端socket,端口和ip
listen(server_socket);//监听server_socket
while(1)//某种情况退出
{
	l_socket.clear();
	int new_socket = accept();//获取连接的客户端socket
    while(1)//某种情况退出
	{
		if(l_socket.size() <= 2)//一个线程处理2个客户端socket
		{
			l_socket.posh_back(skt);
		}
		else
		{
			break;
		}
		//加一个定时器。防止break条件不触发
		ifwhile循环的时间超过10ms)
		{
			break;
		}
    }
    if( l_socket.size() > 0)
    {
    	new pthread(new_socket,l_socket);//线程函数中传入与客户端队列
    }	
}
//假如线程函数是new_socket_pthread
void new_socket_pthread(list<Socket > l_socket;)
{
	forint i = 0; i < l_socket.size();i++//处理客户端socket
	{
		while(1)
		{
			read();
			write();
		}
		close();
	}
}

这个时候可以处理服务端绑定一个端口的情况,如果服务器要绑定两个端口会出现什么情况呢?

服务器绑定两个端口

int server_socket_A = socket();//创建socket
bind(server_socket_A,);//绑定服务端socket,端口A和ip
listen(server_socket_A);//监听server_socket

int server_socket_B = socket();//创建socket
bind(server_socket_B,);//绑定服务端socket,端口B和ip
listen(server_socket_B);//监听server_socket
while(1)//某种情况退出
{
	int new_socket = accept();//获取连接端口A的客户端socket 
	//
	//.....各种处理
	//
	
	int new_socket = accept();//获取连接端口B的客户端socket
	//
	//.....各种处理
	//
	
}

前提: accept如果没有可以连接的socket就会阻塞
场景: 服务端接收到了端口B的客户端连接,但是没有收到端口A的客户端连接。
问题: 这时候会被阻塞在第一个accept,从而导致端口B的客户端连接也无法处理。

解决办法: 使用非阻塞的socket

使用select

int server_socket_A = socket();//创建socket
bind(server_socket_A,);//绑定服务端socket,端口A和ip
listen(server_socket_A);//监听server_socket

int server_socket_B = socket();//创建socket
bind(server_socket_B,);//绑定服务端socket,端口B和ip
listen(server_socket_B);//监听server_socket

int flags = fcntl(sock_fd, F_GETFL, 0);
fcntl(server_socket_A , F_SETFL, flags|O_NONBLOCK);//设置非阻塞,此方法在read和write的时候需要增加参数
fcntl(server_socket_B , F_SETFL, flags|O_NONBLOCK);
//ioctl(server_socket_A , FIONBIO, 1);  //或者ioctl设置非阻塞1:非阻塞 0:阻塞
//ioctl(server_socket_B , FIONBIO, 1);  
fd_set allset;
struct timeval tv = {0, 10000};		//set timeval(10ms)
while(1){
        FD_ZERO(&allset);
        FD_SET(server_socket_A , &allset);
        FD_SET(server_socket_B , &allset);
        select(server_socket_A +1, &allset, NULL, NULL, &tv);
        select(server_socket_B +1, &allset, NULL, NULL, &tv);
 
        if(FD_ISSET(server_socket_A , &allset)){
            int clientfd = accept(server_socket_A , NULL, NULL);
            pthread_t thread;
            pthread_create(&thread, NULL, 线程函数, (void *)&clientfd);//可以像上边做成一个线程处理多个客户端
        }
        if(FD_ISSET(server_socket_B , &allset)){
            int monitorfd = accept(server_socket_B , NULL, NULL);
            pthread_t thread;
            pthread_create(&thread, NULL, 线程函数, (void *)&monitorfd);//可以像上边做成一个线程处理多个客户端
        }
    }

设置阻塞的方法有两种:

  1. fcntl(server_socket_A , F_SETFL, flags|O_NONBLOCK);//设置非阻塞,此方法在read和write的时候需要增加参数
  2. ioctl(server_socket_A , FIONBIO, 1); //ioctl设置非阻塞1:非阻塞 0:阻塞

connect非阻塞模式

借鉴于:https://blog.csdn.net/nphyez/article/details/10268723

客户端

int sock_fd;
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
/* 设置非阻塞模式方法一,此方法在read和write的时候需要增加参数 */
int flags = fcntl(sock_fd, F_GETFL, 0)
fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK);
/* 设置非阻塞模式方法一  */
//int imode = 1;
//ioctl(sock_fd, FIONBIO, &imode);
int ret = connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr));
if (0 == res)
{
  printf("socket connect succeed immediately.\n");
  ret = 0;
}
else
{
  printf("get the connect result by select().\n");//
  if (errno == EINPROGRESS)
  {
    fd_set rfds, wfds;
	struct timeval tv;//超时时间 
	tv.tv_sec = 10; 
	tv.tv_usec = 0;
	FD_ZERO(&rfds);FD_ZERO(&wfds);
	FD_SET(sock_fd, &rfds);
	FD_SET(sock_fd, &wfds);
	int selres = select(sock_fd + 1, &rfds, &wfds, NULL, &tv);
	switch (selres)
	{
	    case -1:
	        printf("select error\n");
	        ret = -1;
	        break;
	case 0:
	       printf("select time out\n");
	       ret = -1;
	       break;
	default://socket描述符可读可写
	       if (FD_ISSET(sock_fd, &rfds) || FD_ISSET(sock_fd, &wfds))
	       {
	       //再次调用connect,相应返回失败,如果错误errno是EISCONN,表示socket连接已经建立,否则认为连接失败。
	          connect(sock_fd, (struct sockaddr *)&addr,sizeof(struct sockaddr_in));
              int err = errno;
               if  (err == EISCONN)
               {
                   printf("connect sucess.\n");
                   ret = 0;
               }
               else
               {
                   printf("connect failed. errno = %d\n", errno);
                   printf("FD_ISSET(sock_fd, &rfds): %d\n FD_ISSET(sock_fd, &wfds): %d\n",FD_ISSET(sock_fd, &rfds) , FD_ISSET(sock_fd, &wfds));
                   ret = errno;
               }

	       }
		}
	  }
}

connect会立即返回,可能返回成功,也可能返回失败。如果连接的服务器在同一台主机上,那么在调用connect 建立连接时,连接通常会立即建立成功(我们必须处理这种情况)。如果connect返回失败,可能正在连接但是没有完全连接,也有可能就是失败了,这时候需要select来确实失败的具体情况。

select判断规则:

1)如果select()返回0,表示在select()超时,超时时间内未能成功建立连接,也可以再次执行select()进行检测,如若多次超时,需返回超时错误给用户。

2)如果select()返回大于0的值,则说明检测到可读或可写的套接字描述符。源自 Berkeley 的实现有两条与 select 和非阻塞 I/O 相关的规则:

A) 当连接建立成功时,套接口描述符变成 可写(连接建立时,写缓冲区空闲,所以可写)
B) 当连接建立出错时,套接口描述符变成 既可读又可写(由于有未决的错误,从而可读又可写)

因此,当发现套接口描述符可读或可写时,可进一步判断是连接成功还是出错。这里必须将B和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时select同样会返回非阻塞socket描述符既可读又可写。

非阻塞socket的read和write

在这里插入图片描述

write为什么会阻塞

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
首先,write成功返回,只是buf中的数据被复制到了kernel中的TCP发送缓冲区。至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。

write在什么情况下会阻塞
当kernel的该socket的发送缓冲区已满时。对于每个socket,拥有自己的send buffer和receive buffer。两个缓冲区大小都由系统来自动调节。

已经发送到网络的数据依然需要暂存在send buffer中,只有收到对方的ack后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间。接收端将收到的数据暂存在receive buffer中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive buffer填满,由于TCP的滑动窗口和拥塞控制,接收端会阻止发送端向其发送数据。这些控制皆发生在TCP/IP栈中,对应用程序是透明的,应用程序继续发送数据,最终导致send buffer填满,write调用阻塞。

read为什么会阻塞
在receive buffer为空时,blocking模式才会等待,所以阻塞了

一般来说,由于接收端进程从socket读数据的速度跟不上发送端进程向socket写数据的速度,最终导致发送端write调用阻塞。

read和write的特点

  1. read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。只有当receive buffer为空时,blocking模式才会等待,而nonblock模式下会立即返回-1(errno = EAGAIN或EWOULDBLOCK)
  2. blocking的write只有在缓冲区足以放下整个buffer时才返回(与blocking read并不相同)
  3. nonblock write则是返回能够放下的字节数,之后调用则返回-1(errno = EAGAIN或EWOULDBLOCK)
  4. 对于blocking的write有个特例:当write正阻塞等待时对面关闭了socket,则write则会立即将剩余缓冲区填满并返回所写的字节数,再次调用则write失败(connection reset by peer)

对应用程序来说,与另一进程的TCP通信其实是完全异步的过程:

  1. 我并不知道对面什么时候、能否收到我的数据

  2. 我不知道什么时候能够收到对面的数据

  3. 我不知道什么时候通信结束(主动退出或是异常退出、机器故障、网络故障等等)

对于1和2,采用write() -> read() -> write() -> read() ->…的序列,通过blocking read或者nonblock read+轮询的方式,应用程序基于可以保证正确的处理流程。

对于3,kernel将这些事件的“通知”通过read/write的结果返回给应用层。

其他的:
设置心跳

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值