一个客户端,服务端只监听一个端口
客户端伪代码
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
总结:
- 客户端的三次握手发生在connect,服务端三次握手发生在accept。
- connect和accept都是阻塞函数。
- 阻塞函数的意思是,当前线程被阻塞,这个线程的其他业务都无法展开。
- 此场景太过简单,没有什么应用意义。
- 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条件不触发
if(while循环的时间超过10ms)
{
break;
}
}
if( l_socket.size() > 0)
{
new pthread(new_socket,l_socket);//线程函数中传入与客户端队列
}
}
//假如线程函数是new_socket_pthread
void new_socket_pthread(list<Socket > l_socket;)
{
for(int 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);//可以像上边做成一个线程处理多个客户端
}
}
设置阻塞的方法有两种:
- fcntl(server_socket_A , F_SETFL, flags|O_NONBLOCK);//设置非阻塞,此方法在read和write的时候需要增加参数
- 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的特点
- read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。只有当receive buffer为空时,blocking模式才会等待,而nonblock模式下会立即返回-1(errno = EAGAIN或EWOULDBLOCK)
- blocking的write只有在缓冲区足以放下整个buffer时才返回(与blocking read并不相同)
- nonblock write则是返回能够放下的字节数,之后调用则返回-1(errno = EAGAIN或EWOULDBLOCK)
- 对于blocking的write有个特例:当write正阻塞等待时对面关闭了socket,则write则会立即将剩余缓冲区填满并返回所写的字节数,再次调用则write失败(connection reset by peer)
对应用程序来说,与另一进程的TCP通信其实是完全异步的过程:
-
我并不知道对面什么时候、能否收到我的数据
-
我不知道什么时候能够收到对面的数据
-
我不知道什么时候通信结束(主动退出或是异常退出、机器故障、网络故障等等)
对于1和2,采用write() -> read() -> write() -> read() ->…的序列,通过blocking read或者nonblock read+轮询的方式,应用程序基于可以保证正确的处理流程。
对于3,kernel将这些事件的“通知”通过read/write的结果返回给应用层。
其他的:
设置心跳