转自:http://blog.youkuaiyun.com/wzhwho/article/details/6169620
一、socket流程
socket 是面向客户/服务器模型而设计的,
针对客户和服务器程序提供不同的socket 系统调用
二、长连接和短连接
长连接:在多次请求中保持连接,使用同一个连接处理多次请求,直至出现错误或者异常才断开,并重新建立新的连接。
一般通过服务器端的长时间的读超时和客户端重用连接来实现。
典型例子:ui->as
短连接:每个请求建立一个连接,请求处理完成,则断开连接。
一般服务器端使用短的读超时。
典型例子:browser->apache
ependingpoll同时支持长连接和短链接
三、常见问题
1.socket不够
大压力短连接,出现大量close_wait
解决方法:
lg.l_onoff = 1;
lg.l_linger = 0;
setsockopt(svrsock->sock, SOL_SOCKET, SO_LINGER,
2.长连接请求混乱
长连接请求错乱,收到其他线程的请求
导致原因:在长连接出错的情况下,并没有关闭连接
3.SIGPIPE信号
向断开(半关闭)的连接中write数据时产生
通常处理方式:
signal(SIGPIPE, SIG_IGN);
SIGPIPE信号被忽略
4.TCP_NODELAY
TCP_NODELAY 不使用Nagle算法,不会将小包进行拼接成大包再进行发送,直接将小包发送出去,会使得小包时候用户体验非常好。
如果没有TCP_NODELAY在压力的情况下,会有延时(40ms)
Ependingpool自己默认的accept函数没有将socket设置成TCP_NODELAY.需要自己写回调函数来控制。
加入如下语句
client = ul_accept(sock, (struct sockaddr *)&sin, &slen);
5.SO_REUSEADDR
通常用于服务监听套接字,支持服务快速重启
如:
// 地址复用
int on = 1;
setsockopt(listen_sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
6.一次读写不完全
正常情况下,read和write可能读写比指定数量少的数据
原因可能是:
内核缓冲区满
被信号中断
解决方法:
反复调用直到数据被全部读(写)完(注意死循环)
7.网络字节序转换
big-endian和little-endian
相关函数:
htonl、htons、ntohl、ntohs
保证程序兼容性和可移植性
8.select中的bug
不能操作多于1024个的socket
FD_SETSIZE在内核中定义为1024,并使用其声明最大的描述字集大小
解决方法:
调整程序,使其所需socket少于1024
使用poll()来代替select()
9.带超时的select
将要读的套接字加入rset中
调用select()。如果返回0则超时,返回-1则出错,否则利用FD_ISSET宏检查rset中的该套接字是否置位,如果是,则该套接字有数据可读,调用read()来读取
写超时控制和读超时控制的操作类似,但设置的是wret
10.带超时的Connect
设置套接字为非阻塞:
flags=fcntl(sockfd,F_GETFL,0);
fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);
调用connect()。如果返回成功则连接已经建立。不成功则检查errno,如果errno为EINPROGRESS,表示连接正在试图建立中。其他错误则应返回出错。
将套接字加入rset和wset,调用select()。返回0则表示超时。
如果select()返回成功,则检查sockfd的状态,仅可写则为连接建立成功,可读且可写表示出错。
恢复套接字的原有状态:
fcntl(sockfd,F_SETFL,flags);
11.调用被信号中断
大多数的阻塞系统调用都可能被信号中断
read()、write()、accept()、select()、connect()……
恢复被中断的系统调用
设置信号标志为SA_RESTART(并不是所有系统都支持;并不是所有系统调用都支持)
判断errno为EINTR,则再次调用函数(推荐)
read()、write()、select()、accept()可以重新调用一次:
do {
}while ((result == -1) && (errno == EINTR));
connect()被中断不能重新调用,只能使用select()来等待连接完成。
http://www.dewen.org/q/558/关于判断tcp连接断开的问题?
http://bbs.chinaunix.net/thread-1526372-1-1.html
http://www.unixresources.net/linux/clf/program/archive/00/00/38/43/384345.html
http://groups.google.com/group/dev4server/browse_thread/thread/ed7f69471d960996
这是根据自己的笔记整理的,如有错误,欢迎指出来.
tcp协议本身是可靠的,并不等于应用程序用tcp发送数据就一定是可靠的.不管是否阻塞,send发送的大小,并不代表对端recv到多少的数据.
在阻塞模式下,send函数的过程是将应用程序请求发送的数据拷贝到发送缓存中发送并得到确认后再返回.但由于发送缓存的存在,表现为:如果发送缓存大小比请求发送的大小要大,那么send函数立即返回,同时向网络中发送数据;否则,send向网络发送缓存中不能容纳的那部分数据,并等待对端确认后再返回(接收端只要将数据收到接收缓存中,就会确认,并不一定要等待应用程序调用recv);
在非阻塞模式下,send函数的过程仅仅是将数据拷贝到协议栈的缓存区而已,如果缓存区可用空间不够,则尽能力的拷贝,返回成功拷贝的大小;如缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.
linux下可用sysctl -a | grep net.ipv4.tcp_wmem查看系统默认的发送缓存大小:
net.ipv4.tcp_wmem = 4096 16384 81920
这有三个值,第一个值是socket的发送缓存区分配的最少字节数,第二个值是默认值(该值会被net.core.wmem_default覆盖),缓存区在系统负载不重的情况下可以增长到这个值,第三个值是发送缓存区空间的最大字节数(该值会被net.core.wmem_max覆盖).
根据实际测试,如果手工更改了net.ipv4.tcp_wmem的值,则会按更改的值来运行,否则在默认情况下,协议栈通常是按net.core.wmem_default和net.core.wmem_max的值来分配内存的.
应用程序应该根据应用的特性在程序中更改发送缓存大小:
socklen_t sendbuflen = 0; socklen_t len = sizeof(sendbuflen); getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len); printf("default,sendbuf:%d\n", sendbuflen); sendbuflen = 10240; setsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, len); getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len); printf("now,sendbuf:%d\n", sendbuflen); |
需要注意的是,虽然将发送缓存设置成了10k,但实际上,协议栈会将其扩大1倍,设为20k.
-------------------实例分析----------------------
在实际应用中,如果发送端是非阻塞发送,由于网络的阻塞或者接收端处理过慢,通常出现的情况是,发送应用程序看起来发送了10k的数据,但是只发送了2k到对端缓存中,还有8k在本机缓存中(未发送或者未得到接收端的确认).那么此时,接收应用程序能够收到的数据为2k.假如接收应用程序调用recv函数获取了1k的数据在处理,在这个瞬间,发生了以下情况之一:
A. 发送应用程序认为send完了10k数据,关闭了socket:
发送主机作为tcp的主动关闭者,连接将处于FIN_WAIT1的半关闭状态(等待对方的ack),并且,发送缓存中的8k数据并不清除,依然会发送给对端.如果接收应用程序依然在recv,那么它会收到余下的8k数据(这个前题是,接收端会在发送端FIN_WAIT1状态超时前收到余下的8k数据.),然后得到一个对端socket被关闭的消息(recv返回0).这时,应该进行关闭.
B. 发送应用程序再次调用send发送8k的数据:
假如发送缓存的空间为20k,那么发送缓存可用空间为20-8=12k,大于请求发送的8k,所以send函数将数据做拷贝后,并立即返回8192;
假如发送缓存的空间为12k,那么此时发送缓存可用空间还有12-8=4k,send()会返回4096,应用程序发现返回的值小于请求发送的大小值后,可以认为缓存区已满,这时必须阻塞(或通过select等待下一次socket可写的信号),如果应用程序不理会,立即再次调用send,那么会得到-1的值,在linux下表现为errno=EAGAIN.
C. 接收应用程序在处理完1k数据后,关闭了socket:
接收主机作为主动关闭者,连接将处于FIN_WAIT1的半关闭状态(等待对方的ack).然后,发送应用程序会收到socket可读的信号(通常是select调用返回socket可读),但在读取时会发现recv函数返回0,这时应该调用close函数来关闭socket(发送给对方ack);
如果发送应用程序没有处理这个可读的信号,而是继续调用send,那么第一次会像往常一样继续填充缓存区,然后返回,但如果再次调用send,进程会收到SIGPIPE信号,该信号的默认响应动作是退出进程.
D. 交换机或路由器的网络断开:
接收应用程序在处理完已收到的1k数据后,会继续从缓存区读取余下的1k数据,然后就表现为无数据可读的现象,这种情况需要应用程序来处理超时.一般做法是设定一个select等待的最大时间,如果超出这个时间依然没有数据可读,则认为socket已不可用.
发送应用程序会不断的将余下的数据发送到网络上,但始终得不到确认,所以缓存区的可用空间持续为0,这种情况也需要应用程序来处理.
如果不由应用程序来处理这种情况超时的情况,也可以通过tcp协议本身来处理,具体可以查看sysctl项中的:
net.ipv4.tcp_keepalive_intvl
net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time
所以,要想编写优秀的socket程序也是很不容易的.特别是在为应用做优化时,很多工作都非常的烦琐.
main() -> standolone_main() -> listen daemon
|-> two_porccess_start()
or->one_process_start()
then prelogin -> login -> postlogin(FTP session )
今天看 VSFTPD的源代码,真是太爽了,写得真好.