首先我们来看两个例子;
第一个例子:
客户端代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <memory.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#define PORT 9999
#define BUFFER 1024
int main(int argc, char *argv[])
{
struct sockaddr_in server_addr;
int n, count = 0;
char buf[BUFFER];
int sockfd = socket(AF_INET,SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("create socket error\n");
return -1;
};
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (connect(sockfd, (struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
{
perror("connect error");
return -1;
}
memset(buf,0,sizeof(BUFFER));
while(1)
{
sleep(10);
exit(1);
}
close(sockfd);
}
服务端代码
//server
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define PORT 9999
#define BACKLOG 5
#define buflen 1024
int main(int argc,char **argv)
{
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int listenfd = socket(AF_INET, SOCK_STREAM,0);
if (listenfd < 0)
{
perror("create socket error.\n");
return -1;
}
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listenfd,(struct sockaddr*)&server_addr,sizeof(server_addr)) < 0)
{
perror("bind error.\n");
return -1;
}
if (listen(listenfd, BACKLOG) < 0)
{
perror("listen error.\n");
return -1;
}
pid_t pid;
int n, count = 0;
char buf[buflen];
while(1)
{
socklen_t addrlen = sizeof(client_addr);
int connfd = accept(listenfd, (struct sockaddr*)&client_addr,&addrlen);
if (connfd < 0)
{
perror("accept error.\n");
}
printf("receive connection\n");
memset(buf,'a',buflen);
while( (n = write(connfd ,buf, buflen) )> 0)
{
count++;
printf("already write %d bytes --- %d\n",n,count);
}
if (n < 0)
{
printf("write error.\n");
exit(0);
}
}
}
结果为
从上看出服务端在写了2577之后,就陷入阻塞。注意 此时发送缓冲区并不一定是满了,只能说明套接字中发送缓冲区剩余空间小于write请求写的字节数(这里是1024)
而当客户端sleep之后,服务端退出进程
第二个例子
我们将上面的服务端改成非阻塞模式:
//server
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#define PORT 9999
#define BACKLOG 5
#define buflen 1024
int main(int argc,char **argv)
{
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int listenfd = socket(AF_INET, SOCK_STREAM,0);
if (listenfd < 0)
{
perror("create socket error.\n");
return -1;
}
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listenfd,(struct sockaddr*)&server_addr,sizeof(server_addr)) < 0)
{
perror("bind error.\n");
return -1;
}
if (listen(listenfd, BACKLOG) < 0)
{
perror("listen error.\n");
return -1;
}
pid_t pid;
int n, count = 0;
char buf[buflen];
while(1)
{
socklen_t addrlen = sizeof(client_addr);
int connfd = accept(listenfd, (struct sockaddr*)&client_addr,&addrlen);
if (connfd < 0)
{
perror("accept error.\n");
}
printf("receive connection\n");
memset(buf,'a',buflen);
int flag = fcntl(listenfd, F_GETFL,0);
flag = flag | O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
while( (n = write(connfd ,buf, buflen) )> 0)
{
count++;
printf("already write %d bytes --- %d\n",n,count);
}
if (n < 0)
{
if (errno == EWOULDBLOCK)
{
printf("EWOULDBLOCK error.\n");
}else{
perror("write error");
}
}
exit(1);
close(listenfd);
}
}
结果为
总结:
从上面两个例子可以看出:
(1)数据由应用缓冲区复制到发送缓冲区(内核中),write返回成功仅仅表示数据成功的应用程序复制到套接字发送缓冲区
(2)内核协议栈将套接字中的发送缓冲区中的数据发送到对端主机,注意这个过程不受应用程序控制
(3)数据到达接受端套接字缓冲区(内核中),这个过程也不受应用程序控制
(4)应用程序调用read函数,将套接字缓冲区的数据复制到应用程序中
第一个例子中当发送缓冲区满(发送缓冲区剩余空间大小小于write发送数据字节大小),write函数处于阻塞,第二个例子中,当发送缓冲区满(发送缓冲区剩余空间大小小于write发送数据字节大小),write函数在调用后返回EWOULDBLOCK.
因此当客户端因为某些原因移植不从socket缓冲区中收取数据,服务端可能定期定期产生一些数据发送给客户端,一段时间之后,由于TCP窗口太小,导致数据发送不出去,待发送的数据会在服务端中发送缓冲区中堆积,如果不做处理,将会不断申请空间来存放待发的数据,最终导致内存消耗完,操作系统将服务端杀死。
对于以上的这种情况,可以采用的措施:
1.设置每路发送缓冲区大小的上限(如1M),将数据(该数据大小远小于缓冲区设置上限)存入发送缓冲区时,先判断一下缓冲区最大剩余空间,如果剩余空间已经小于要放入的数据大小,则认为连接出现问题,关闭该路连接并回收相应的资源(如果清空缓冲区的大小,回收套接字)。
//writeBuffer为发送缓冲区
size_t remainLen = writeBuffer.remainBytes();
//sendData为需要发送的数据大小
if (remainLen < sendData.length())
{
Close(); //关闭套接字
writeBUffer.clear(); //清空发送缓冲区
}
writeBuffer.send(sendData.c_str(), sendData.length());
2.当然也有另一种情况,数据写入发送缓冲区,此时服务器未产生新的待发数据,客户端又一直不接受,服务器如果不去检测发送缓冲区中堆积的数据,那么将会白白浪费系统资源。对于这种情况我们就会设置一个定时器,每=每隔一段时间就去检查各路连接的发送缓冲区是否还有数据未发送出去,如果一个连接超过一定时间内还存在未发送出去的数据,就认为该连接出现问题,我们可以关闭改路连接并回收相应的资源(清空缓冲区、回收套接字资源)