Chinaunix首页 | 论坛 | 博客
  • 博客访问: 309420
  • 博文数量: 21
  • 博客积分: 250
  • 博客等级: 二等列兵
  • 技术积分: 484
  • 用 户 组: 普通用户
  • 注册时间: 2010-10-06 23:10
个人简介

程序猿

文章分类

全部博文(21)

文章存档

2016年(17)

2014年(3)

2013年(1)

分类: 网络与安全

2016-01-02 16:39:53

先看代码。(完整的代码可到下载)
server.c
  1. int main(void)
  2. {
  3.     int listenfd;
  4.     if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
  5.         ERR_EXIT("socket error");

  6.     struct sockaddr_in servaddr;
  7.     memset(&servaddr, 0, sizeof(servaddr));
  8.     servaddr.sin_family = AF_INET;
  9.     servaddr.sin_port = htons(5188);
  10.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

  11.     int on = 1;
  12.     if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
  13.         ERR_EXIT("setsockopt error");

  14.     if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
  15.         ERR_EXIT("bind error");

  16.     if (listen(listenfd, SOMAXCONN) < 0)
  17.         ERR_EXIT("listen error");

  18.     while (1) {
  19.         int conn;
  20.         struct sockaddr_in peeraddr;
  21.         socklen_t peerlen = sizeof(peeraddr);
  22.         if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
  23.             ERR_EXIT("accept error");
  24.         printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

  25.         while (1) {
  26.             char recvbuf[1024];
  27.             int readlen, writelen;

  28.             readlen = recv(conn, recvbuf, sizeof(recvbuf), 0);
  29.             if (readlen <= 0)
  30.                 break;
  31.             writelen = send(conn, recvbuf, readlen, 0);
  32.             if (writelen < readlen)
  33.                 break;
  34.         }
  35.         printf("close\n");
  36.         close(conn);
  37.     }
  38.     close(listenfd);

  39.     return 0;
  40. }
client.c
  1. int main(int argc, const char *argv[])
  2. {
  3.     int sock;
  4.     int size = 1024;
  5.     char *buffer;

  6.     if (argc > 1) {
  7.         size = atoi(argv[1]);
  8.         printf("size = %d\n", size);
  9.     }
  10.     buffer = malloc(size);
  11.     if (buffer == NULL)
  12.         ERR_EXIT("malloc error");
  13.     if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
  14.         ERR_EXIT("socket error");

  15.     struct sockaddr_in servaddr;
  16.     memset(&servaddr, 0, sizeof(servaddr));
  17.     servaddr.sin_family = AF_INET;
  18.     servaddr.sin_port = htons(5188);
  19.     servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

  20.     if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
  21.         ERR_EXIT("connect error");

  22.     int writelen, readlen;

  23.     printf("write...\n");
  24.     writelen = send(sock, buffer, size, 0);

  25.     printf("read...\n");
  26.     while (readlen < writelen) {
  27.         int templen = recv(sock, buffer, size, 0);
  28.         if (templen <= 0) {
  29.             ERR_EXIT("read error");
  30.         }
  31.         readlen += templen;
  32.     }

  33.     printf("close\n");
  34.     close(sock);

  35.     return 0;
  36. }
代码很简单,服务器以1024字节为单位接收客户端发送过来的数据,并马上发送回去。客户端发送指定大小的数据,并等待接收。

这个程序初看起来没有问题。
我们来运行一下:
# ./client
write...
read...
close

确实没问题。
再来改变下客户端发送的数据大小:
# ./client 5000000
size = 5000000
write...
read...
close

# ./client 50000000
size = 50000000
write...

我们发现,当客户端发送数据量设置为50000000时,客户端和服务器都阻塞了!

用netstat命令看一下:
# netstat anp | grep 5188
tcp   6072740 2607544 localhost:5188          localhost:60860         ESTABLISHED
tcp   953090 4163264 localhost:60860         localhost:5188          ESTABLISHED

其中,第2列表示接收队列中剩余数据大小,第3列表示发送队列中剩余数据大小。多次运行该命令,这两列的数字不会变。这说明数据已不再进行收发。

先画个简图:
C           S
send        
           recv 1024B
           send 1024B
           recv 1024B
           send 1024B
           ...
recv

这个问题产生的原因是这样的:
客户端调用send发送数据时,需要先把数据拷贝到发送缓冲区。对于阻塞IO,当发送缓冲区已满,还有数据未拷贝时,会一直等待。
上面的例子中,客户端需要发送大量数据,此数据量大于发送缓冲区的大小,于是,客户端阻塞。服务器每接收1024字节就往发送缓冲区中填1024字节数据,此时由于客户端未调用recv函数,服务器端的发送缓冲区不能减少。当服务器的发送缓冲区满,客户端数据还无法都填入发送缓冲区时,程序就走不下去了。

可以使用命令sysctl 查看发送缓冲区的大小。
# sysctl -A|grep tcp_wmem
net.ipv4.tcp_wmem = 4096        16384   4194304
3个数字分别表示,为TCP连接分配的最小内存,缺省内存和最大内存。


如何避免这个问题?
1. 修改发送缓冲区大小
但这个方法治标不治本,当数据量更大时又有同样问题。
2. 阻塞IO改成非阻塞IO
这样send不会阻塞,当send返回失败时,可以先调用recv把对方的发送缓冲区清空。
3. 修改收发协议
在数据头部增加数据长度信息,服务器先接收头部信息,根据长度分配buffer,再一次接收此长度的数据。


阅读(4359) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

不懂IT2016-06-05 23:28:42

写的恨得很详细,一个问题也讲的很完整,而且还要补充的代码说明(为了写这个,还自己去另一个全英文网站补充完整的代码)

排版很简洁,句子一个形容词都没有,标准的论文文体,一个错别字都没有!

郑老师果然是个好老师,这么细致!

虽然我听不懂在讲什么,但是凭感觉,觉得你写的是很实在[/表情71

教程如此详细,让我忍不住想从零学代码。