socket编程注意事项

1 进程间通信的方式

常用的进程间通信的方式有:管道(pipe),命名管道(named pipe),信号,消息队列,共享内存,信号量等,这些通信基本是本机进程之间的,在网络中的主机之间是无法适用的。

2 套接字socket

套接字的出现主要解决网络间各主机间进程通信的问题,换而言之现在所以网络中的进程通信都采用socket方式。
1.2.1 套接字的分类
SOCK_STREAM
流套接字,提供面向连接、可靠的数据传输服务,数据按字节流、按顺序收发,保证在传输过程中无丢失、无冗余。TCP协议支持该套接字。

SOCK_DEGRAM
数据报套接字,提供面向无连接的服务,数据收发无序,不能保证数据的准确到达。UDP协议支持该套接字。

SOCK_RAM
原始套接字,原始套接字。允许对低于传输层的协议或物理网络直接访问,例如可以接收和发送ICMP报文。常用于检测新的协议。

NETLINK
Netlink套接字,属于原始套接字,用于在内核模块与在用户地址空间中的进程之间的通信,比如防火墙,SELinux 事件通知等都是采用NETLINK通信方式。

3 socket通信的API(udp,tcp)

......

4 Socket通信需要注意的问题

4.1 阻塞和非阻塞
阻塞:执行调用后,直到返回调用结果才结束,否则一直等待结果。
如read(),write(),send(),sendto(),recv(),recvfrom().
非阻塞:执行调用后立即返回。

一般我们都希望写出非阻塞的socket,所以我们可以阻塞的函数+select来写出非阻塞的socket。
也可以这么来写出非阻塞的read:
if ((nread = read(sock_fd, buffer, len)) < 0)
{
if (errno == EWOULDBLOCK)
{
return 0; //表示没有读到数据
}else return -1; //表示读取失败
}else return nread;读到数据长度
EWOULDBLOCK表示操作本来应该阻塞的

4.2 地址端口复用
SO_REUSEADDR,SO_REUSEPORT
TCP协议规定,主动关闭连接的一方处于TIME_WAIT状态,等待2MSL时间后才能回到CLOSED状态,如果CTRL-C终止了server,server就是主动关闭的一方,这样在TIME_WAIT期间就不能再次监听同样的server端口。这个时候你如果启动server会报 address already in used的错误,errno号为98。
解决办法是通过setsockopt选项设置 SO_REUSEADDR。并且在bind之前。这样当服务器断开之后还可以立即起来。
MSL为报文在网络中的最大生存时间。RFC 793指出MSL为2分钟,现实中常用30秒或1分钟。

4.3 字节序的转换
由于大小端的问题,本机使用主机字节序,传输的时候需要使用网络字节序。
4.4 全双工,收发可以同时进行
由于socket是全双工的字节流,全双工意味着接收的时候也可以同时发送,发送的时候也可以同时接收。因此,如果软件要求高性能,一般采用多线程处理,才能达到收和发同时进行的效果。

4.5 屏蔽信号
SIGEPIPE必须屏蔽,其他的信号是不是需要处理要看具体情况,如SIGCHILD,SIGINT,SIGTERM等。
SIGPIPE   引起程序崩溃的一个信号
SIGINT   CTRL-C会发此信号
SIGCHILD  子进程退出时会向父进程发sigchild信号,父进程收到之后来回收子进程。避免产生大量的僵尸进程。
僵尸进程:子进程退出,父进程没有调用wait或waitpid来处理子进程的状态信息,那么子进程的进程描述符仍在系统中,这样的进程成为僵尸进程。
孤儿进程:父进程结束,子进程便沉孤儿进程,并且由系统的init进程来托管。

4.6 缓冲区大小
每个TCP SOCKET在内核中都有一个发送缓冲区和接收缓冲区,TCP全双工模式和TCP的滑动窗口就依赖这两个独立的buffer和buffer的填充状态。
应用程序调用send,write仅仅是将数据从buffer拷贝到了缓冲区(所以当send,write返回时数据不一定真的发送了),在合适的时机,内核将发送缓冲区的数据发送到对方的接受缓冲区。
接收和发送类似。
如果发送端特别快,缓冲区很快被填满(socket默认1024*8=8192字节)这个时候根据实际情况设置缓冲区的大小,使用setsockopt,也可以查看看缓冲区的大小,通过getsockopt,
getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen); 
setsockopt(s,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen);

如果应用窗口一直没有读取,buffer满了之后发生的动作是:通知对端TCP协议中的窗口关闭,这个便是滑动窗口的实现,保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输,因为对方不允许发送超过通告窗口大小的数据,这就是TCP流量控制,如果对方无视告知窗口大小,而发送了超过窗口大小的数据,TCP则丢弃他。
UDP只有接收缓冲区,没有发送缓冲区,有数据就直接发送,不管对方是否能够正确接收,也不管对方缓冲区是否满了,
UDP没有流量控制,快的发送者很容易淹没慢的接收者,导致接收放的UDP丢弃数据包。

4.7 Accept耗尽句柄
服务器每次accept一个链接之后,都会分配新的socket资源。Linux下对每个进程所能使用的文件句柄数是有限制的,默认是1024。扣除stdin,stdout,stderr,只有1021个句柄可用。一旦出现了accept的连接数超过了这个限制后就会很尴尬:accept返回-1,errno = 24,即EMFILE。如果采用Select、poll 或者epoll LT方式,系统会不停通知你,但是你没法拿到那个fd,也就不能处理,甚至连close都不能。只能尴尬地让cpu跑在100%,期待着某个客户端断线,某个句柄恰好被close,然后才能处理之。
解决方法:
  直接在代码中限制最大允许连接数,一旦超过则直接踢出。

4.8 Socket资源不释放
服务器未能有效地检测和处理socket关闭事件,导致最后socket资源被耗尽,再也不能连入新的连接。
解决办法:keepalive、网络连接心跳检测,定时关闭不活动的连接。
4.9没处理sigepipe信号导致服务器崩溃
产生原因:
当服务端close了一个连接时,若客户端继续向服务端发送数据,根据TCP协议的的规定,客户端会收到一个RST响应,如果客户端收到这个响应后,继续发送数据,这时候系统会发送一个SIGPIPE信号给客户端进程,这个信号的作用就是终止进程,所以客户端就崩溃退出。
Socket通信的时候,根据TCP协议限制,链路为全双工,所以当关闭本端链路后,对端还是可以接收数据的。
解决方法:
   屏蔽sigpipe信号。
   使用shutdown来关闭两个方向。

4.10发送缓冲使用不当,数据包乱序。
当有数据要发送时,无视发送缓冲直接调用send函数。在同一连接中会发生数据A发送了一部分,又发送数据B的一部分,在发送数据A的一部分。令接收者无所适从。
解决办法:从不直接调用send发送数据,应将发送数据放入发送缓冲中处理。
4.11 网络串包
将A的信息发给了B。通常发生在服务器用socketFd1接受A的请求,服务器处理中。A掉线,服务器close(socketFd1)。这时B再上线,POSIX标准要求每次打开文件的时候必须使用当前最小可用的文件描述符号,于是B使用了socketFd1句柄。服务器就将A的应答发送给了B.

4.12阻塞I/O没有处理EINTR、非阻塞I/O没有处理EAGAIN
ENITR:指操作被中断唤醒,需要重新读/写。
EAGAIN:Linux - 非阻塞socket编程处理EAGAIN错误。EAGAIN的名字叫做EWOULDBLOCK。

对于慢系统调用,像accept, receive这样有可能无法返回的函数的进程一旦捕获了某个信号,并且相应的信号处理函数返回时,该系统调用有可能返回一个EINTR错误。这个时候需要处理这个错误。比如可以这么处理,看如下代码:

While(len > 0){
size = read(fd,buf,sizeof(buf));
If(size <= 0){
If(errno = ENITR)
    continue;
break;
}
len -= size;
memset(buf);
}

非阻塞模式下调用了阻塞操作,在该操作没有完成就返回EAGAIN。EAGAIN不会破坏socket的同步,不用管它。这个错误表示在非阻塞模式下调用了阻塞操作,这个操作一般不用管,接着循环recv就可以了,参考如下代码。

ret = select(....);
If(ret > 0){
While(len > 0){
size = read(fd,buf,sizeof(buf));
If(size <= 0)
{
If((errno == EAGAIN)||(errno ==EWOULDBLOCK ))
continue;
}
len -= size;
memset(buf);
}
}//end while

4.13服务器端太多的TIME_WAIT
主动关闭socket的一方会进入TIME_WIT,并且持续2MSL时间长度,TIME_WAIT一般持续1~4分钟,如果每次关闭的操作都是由服务器发起的,则服务器会存在大量的TIME_WIT,则服务器内核就需要维护大量的状态,
解决办法:
a.使用shutdown(fd,SHUT_WR),通知客户端,由客户端执行close,将TIME_WAIT留在客户端。
b.如果可以,使用长链接,(长链接会增加服务器的压力).

c.设置socket SOL_LINGER选项。

d.客户端机器打开tcp_tw_recycle和tcp_timestamps选项;
e.客户端机器打开tcp_tw_reuse和tcp_timestamps选项;
f.客户端机器设置tcp_max_tw_buckets为一个很小的值

SOL_LINGER的作用:

通常socket使用close后的缺省操作是,如果发送缓冲区中还有数据,则发送完缓冲区中的数据,等待被确认后然后返回。

所以此选项设置后,close关闭后的操作有以下两种:

a.立即关闭该链接,通过发送RST分组来关闭链接而不是FIN|ACK|FIN|ACK四个分组,并丢弃缓冲区中的数据,因此关闭方跳过time_wait,直接进入CLOSED。

b.为关闭的链接设置一个超时,如果超时时间内缓冲区数据发送完成,则按照|FIN|ACK|FIN|ACK|四个分组正常关闭socet.否则使用a来关闭。

在这里粘一段TCP-IP卷1中的关于异常关闭的一段内容:

我们在1 8 . 2节中看到终止一个连接的正常方式是一方发送F I N。有时这也称为有序释放
(orderly release),因为在所有排队数据都已发送之后才发送F I N,正常情况下没有任何数据
丢失。但也有可能发送一个复位报文段而不是F I N来中途释放一个连接。有时称这为异常释放
(abortive release)。
异常终止一个连接对应用程序来说有两个优点:(1)丢弃任何待发数据并立即发送复位
报文段;(2)R S T的接收方会区分另一端执行的是异常关闭还是正常关闭。应用程序使用的
A P I必须提供产生异常关闭而不是正常关闭的手段。
使用s o c k程序能够观察这种异常关闭的过程。Socket API通过“ linger on close”选项
(S O _ L I N G E R)提供了这种异常关闭的能力。我们加上- L选项并将停留时间设为0。这将导
致连接关闭时进行复位而不是正常的F I N

4.14 监听套接字设置为非阻塞

int	st_vod_listenAfterBind(int listenfd)
{
	int	val, ret;

	if(listen(listenfd, LISTENQ) == -1)
	{
		vod_err_ret("listen()");
		return	-1;
	}

	val = fcntl(listenfd, F_GETFL, 0);
	if(val == -1)
	{
		vod_err_ret("fcntl()");
	}

	ret = fcntl(listenfd, F_SETFL, val | O_NONBLOCK);
	if(ret == -1)
	{
		vod_err_ret("fcntl()");
	}

	return	listenfd;
}

常见的网络通信模型都会使用IO多路复用技术如selectpollepoll等。当有新的连接请求到来时,监听套接字变为可读,然后调用accept()接收新连接、返回一个连接套接字。

如果监听套接字是阻塞的,问题可能出在什么地方?

我们知道,connect肯定会先于accept返回。

当一个连接到来的时候,监听套接字可读,此时,我们稍微等一段时间之后再调用accept()。就在这段时间内,客户端设置linger选项(l_onoff = 1, l_linger = 0),然后调用了close(),那么客户端将不经过四次挥手过程,通过发送RST报文断开连接。服务端接收到RST报文,系统会将排队的这个未完成连接直接删除,此时就相当于没有任何的连接请求到来, 而接着调用的accept()将会被阻塞,直到另外的新连接到来时才会返回。这是与IO多路复用的思想相违背的(系统不阻塞在某个具体的IO操作上,而是阻塞在select、poll、epoll这些IO复用上的)。

上述这种情况下,如果监听套接字为非阻塞的,accept()不会阻塞住,立即返回-1,同时errno = EWOULDBLOCK。
 

附件,常见错误:
Time out 
这个timeout涉及范围比较光,比如connect timeout,read timeout等。
Connect timeout 是在三次握手建立连接的过程中,read timeout是在建立连接后,数据交互过程中。
Connect refused
连接的ip或这端口未被监听,会报connnect refused错误
Connect reset by peer和 Connect reset
简单来说就是断开连接后的读写操作引起的。
如果一段的socket被关闭(或主动关闭,或因异常退出引起的关闭),另一端仍发送数据,发送的第一个数据包引发该异常。
一端退出,但退出时未关闭链接,另一端如果通过连接读数据,则会引发connect reset 的错误。如果另一端通过连接写数据则会引发Connect reset by peer。

常见原因:
服务器的并发量超过了其承载量,服务器会主动关闭一些连接。
防火墙问题,如果网络通过防火墙,防火墙一般会有超时机制,在网络长时间不传输数时会关闭这个TCP会话,这个时候在进行读写就会出现这个问题。
如何确定是哪一点关闭了连接:
如果是客户端给服务器发送数据,如果收到了connect reset,则证明服务器关闭了连接,如果是服务器给客户端发送数据,收到了reset信息,则说明客户端关闭了连接。这些也可以从TCPDUMP中看到。
 

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值