标签:
Socket编程FAQ,由Vic Metcalfs创建,这是一系列关于socket编程相关的常问的问题。这些问题整理自comp.unix.programmer新闻组,我省略了其中一些比较基本的socket编程问题,有的问题的解答已经过时,因此我更新了部分回答。
如果对端调用close
或者exit
退出,并且没有设置SO_LINGER
选项,那么本端调用read会返回0,无论调用多少次都返回0第一次调用write后立即返回0,其实在内核层面会收到对端响应的RST
报文,此时如果再次调用write
会导致EPIPE
错误,这个错误会引起SIGPIPE
信号的产生,而这个信号的默认处理方式就是关闭程序。
注:更详细的分析详见网络编程最佳实践
是一个struct sockaddr
类型的参数,这是一个通用的套接字地址结构,根据你的套接字类型,应该传入不同的套接字地址结构,通常会有
sockaddr_in,sockaddr_un
等,为了让bind可以接收不同类型的套接字地址结构就使用了通用类型sockaddr
,通过强制转换,将其转换为
对应的套接字结构来使用。
使用getservbyname
,这个函数会返回一个指向struct servent
结构体的指针,其中有一个字段s_port就是对应服务的端口号了(网络字节序)。
下面是一个小的例子:
//考虑到service可能是一个端口号字符串,会尝试将其转换为网络字节序
int atoport(const char* service,char *proto)
{
int port;
long int lport;
struct servent *serv;
char *errpos;
serv = getservbyname(servivce,proto);
if(serv != NULL)
port = serv->s_port;
else {
lport = strtol(service,&errpos,0)
if((errpos[0] != 0) || (lport < 1) || (lport > 5000))
return -1; //invalid port address
port = htons(lport);
}
return port;
}
如果你选择exit,那么系统会负责将socket描述符关闭,如果你不exit,那么可以手动去调用close来关闭套接字描述符
shutdown
一个典型的应用场景就是向对端发送结束请求(内核负责发送FIN报文),表明请求结束了。close也具备这样的功效,那么close和shutdown的区别和联系是什么呢,close本质上只是减少套接字描述符的引用计数,当引用计数变为0,才会导致TCP/IP的四次挥手过程。因此在多进程场景下,close并不会导致请求结束(也就是导致内核发生四次挥手过程),只是会较少套接字描述符的引用计数。而shutdown则不一样,shutdown会导致内核发起四次挥手过程中的前半部分,也就是半关闭。因为socket是一个类似于pipe的机制,不过socket是双向的,pipe是单向的。通过shutdown可以进行半关闭。也就是关闭了写端,但是仍然可以从socket中读取对端发送过来的数据。
TCP/IP协议就是这样设计的,是不可避免的。主要有两个原因:
TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN,因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。如果A端不维持TIME_WAIT状态,而是处于CLOSED 状态,那么A端将响应RST分节,B端收到后将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个分节任何一个分节的丢失情况,主动关闭连接的A端必须维持TIME_WAIT状态 。
TCP分节可能由于路由器异常而迷途
,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的新连接
,前一个连接
的迷途重复分组在“前一个连接”终止后到达,而被新连接
收到了。为了避免这个情况,TCP协议不允许处于TIME_WAIT
状态的连接启动一个新的可用连接,因为TIME_WAIT
状态持续2MSL
,就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消逝。
因为默认情况下,除了发送数据和ACK外,是没有任何其他数据发送的,因此如果你只是简单的等待对端数据的到来的化,那么如果对端静默的消失了就无法通知你了。通过设置SO_KEEPALIVE
可以进行定期的进行检查,检查连接是否有效。但是需要注意的是这个定期检查的时间间隔至少是2个小时,幸好这个值是可以通过系统参数进行修改的。如果你发送数据给对端,对端响应了ACK那么就表示连接是有效的,但是如果连接是无效的,就会导致数据重传,重传一段时间后你才能得知连接是无效的。大多数互联网程序在服务器端采用超时读的方法,如果在指定时间间隔内没有收到请求那么服务器端就放弃这个客户端。那么如何去维护一个空闲的长连接呢?,通常有两个办法:
使用非阻塞的IO意味着你需要不同的轮询socket去查询它是否有数据,轮询会消耗大量的CPU时间周期。因此应该尽量避免去使用轮询。SIGIO
允许socket有数据可读的时候会由操作系统触发一个SIGIO
的信号,然后在信号处理函数中可以接收数据处理数据,这个方法的缺点就是容易导致数据混乱(不知道是哪个socket的数据),当有多个socket需要读取数据的时候,你应该选择select机制。select可以同时监听多个套接字描述符的数据是否就绪,除此之外select还支持超时机制。
EPROTO
意味着协议发生了不可恢复的错误,通常这个错误发生在accept的时候,在accept未返回之前,这个连接被重置了。
不能强制发送缓冲区中的socket数据,正常情况下调用write写入数据的确会导致TCP发送一个segment
,但是不保证一定会这样。有很多原因会导致TCP不会发送segment, 比如关闭滑动窗口,比如Nagle algorithm
算法等。Nagle
算法会导致发送的多个少量数据被整合成一个包被发送出去。Nagle
算法的出现是为了解决包头负荷的问题,当发送大量小数据的时候,每个小数据都会被附加固定大小的包头那么当要发送的数据小于包头大小的时候,很显然这是一种浪费,因此通过Nagle算法将大量小数据封装在一个包中进行发送。 然后nagle算法对于一些实时性要求高的场合还是不尽如人意的,会导致延迟的问题。可以通过发送带外数据来解决,但是带外数据的内容有限制,一次只能发送1个字节。综上所述要想刷新socket的缓冲,你必须要关闭Nagle
算法。 还有另外一种情况就是当关闭滑动窗口,如果对端不进行数据读取那么发送端的数据就会一直在buffer中直到整个buffer填满,发送端阻塞。
select
的fd_set
是一个bit mask
,有固定大小,从用户空间到内核空间的拷贝开销少,而poll
需要使用者分配pollfd
数组。但是fd_set
有大小限制而pollfd
数组没有大小限制,select
的移植性更好。
对于整型值你需要使用htons
做字节序的转换,而对于字符串来说就是一串单字节流。因此不会有任何问题,如果你要发送的是一个结构体,那么你需要在对端用同样的结构体来接收。在使用的结构体的时候要考虑结构体在不同的OS平台上对齐方式不同,应该通过避免字节对齐来屏蔽这些差异。如果你要发送浮点型的数据,那么可能你需要很多工作要做。
首先你要确定你想使用这个选项,它将会关闭Nagle
算法,导致网络拥堵,浪费带宽。如果通过关闭这个选项没有导致你的速度所有增长,那么请关闭这个选项,通过setsockopt
可以设置TCP_NODELAY
。
read等同与recv的flags参数等于0的情况,write等同于send的flags参数等于0的情况。并且在一个non-unix系统上是不允许在socket上使用write和read,但是send和recv总是可以的。
通常来说只能传递一个信号的数值给信号处理函数,通过sigaction
还可以传递一些额外的参数。我的建议是忽略SIGPIPE
信号。通过errno
值来处理这个错误要比通过信号处理器来处理更好。但是有一种情况需要设置SIGPIPE
为SIG_DFL
,就是当程序将调用exec
簇函数的时候,内核会负责将所有已经设置信号处理函数的信号设置成SIG_DFL
,为了保证其移植性在调用exec簇函数之前,手动设置SIGPIPE
为SIG_DFL
。
在某些类unix的系统上socket函数实际上会可能会打开/dev
下某些特殊文件,因此你需要在chroot环境下创建dev目录和相关的特殊文件。同理一些daemon
的进程会在chroot
后调用syslog
汗水和,这个syslog
函数可能会使用UDP socket
或者FIFO
或者unix socket
等,为了避免在chroot
环境下调用失败,最好在chroot
之前调用openlog
。
这不是一个真正要退出的错误条件,这个错误意味着调用被信号打断了,任何可能会阻塞的调用都应该使用loop包裹起来,然后检查EINTR。
当TCP连接关闭,并且接收到了对端的RST
报文的时候。此时如果调用write
会导致SIGPIPE
错误,但是read是没有问题的,总是返回0.一般出现SIGPIPE
的场景是客户端关闭了连接,但是对端不知情依然向套接字写数据,第一次写入的时候会导致接收到对端发送过来的RST
报文,第二次就会产生EPIPE
错误了。
不想C++里面的异常,socket的异常不是表明发生了错误,socket的异常通常表示带外数据到达的通知。带外数据又称为紧急数据,看起来像是从主的数据流中分离出来的一种数据流。很好的将数据分为了两种不同类型。带外数据又称紧急数据,但是并不是说这种数据传送很快,而是说这种数据的优先级要高,还有一点需要注意的是如果你的程序没有即使读取带外数据,可能会导致数据丢失。而正常的数据流则不会出现丢失的情况。
一些系统的hostname是FQDN
的格式,然而另外一些系统则是unqualifield hotstname
,BIND推荐使用FQDN格式的主机名,但是大多数
Solaris
系统则是趋向于使用unqualifield hotstname
。大多数支持posix语义的系统都提供了uname来获取主机名,但是一些老的BSD系统
仅仅提供gethostname
.通过调用gethostbyname
找到你的ip地址,然后传递给gethostbyaddr
,得到hostent
结构体,然后这个结构体中的
h_name
成员就是你的FQDN
了。
用户输入的地址可能是一个字符串的ip地址,也有可能是一个域名,因此需要考虑这两种情况,下面是例子:
struct in_addr *atoaddr(const char *address)
{
struct hostent *host = NULL;
static struct in_addr saddr;
if(inet_pton(AF_INET, address, &saddr) != -1)
return &addr;
host = gethostbyname(address);
if(host != NULL) {
return (struct in_addr*)*host->h_addr_list;
}
return NULL;
}
只要你在socket
上调用listen
完成后,内核就已经做好接收连接的准备了,通过UNIX
的实现会立即和发起syn
请求的连接完成SYN
握手,然后为其创建socket,并将其放到等待队列中,等待accept
调用。因此在accept前socket已经就绪了。内核会维护两个队列,一个是半连接队列一个是连接队列,当一个连接发起syn
请求后会被放入到半连接队列,等待完成三次握手后会被移入到连接队列中。listen
的第二个参数就是用于设置这个连接队列的大小。当连接的数量超过这个连接队列的大小的时候连接会被忽略,导致对端连接超时,进行超时重连。connect
调用会导致发起syn请求,当对端响应最后一个ACK
的时候,connect
才会返回表示三次握手完成。
因为gethostbyname
的内部实现问题,其内部实现是使用指针指向一个内部的static
的struct hostent
,因此在多次调用gethostbyname
的时候会导致,当前这次的结果覆盖上一次的结果。为了避免这个问题在通过gethostbyname
得到结果后应该拷贝一份出来。
最简单的方式就是connect+alarm
通过alarm定时,时间到了会触发信号,通过信号打断connect调用,从而实现所谓的timeout功能,最好的方式应该是使用select+非阻塞socket
的方式,在使用非阻塞connect的时候,先直接发起connect调用,如果连接成功就不需要使用select,如果没有连接成功再去使用select来接管connect。非阻塞socket连接的时候可能会有下面三种可能:
如果连接成功了,那么select认为是可读(如果有数据到达就是可写),如果连接失败了那么select认为是即可读也可写,并且设置对应的错误码。一个带超时功能的connect代码实现如下:
int connect_timeout(int fd,struct sockaddr_in *addr,int timeout)
{
int ret = 0;
setnonblocking(fd); //setup for nonblocking
socklen_t len = sizeof(struct sockaddr_in);
struct pollfd fds[1];
fds[0].fd = fd;
fds[0].events = POLLOUT;
fds[0].revents = 0;
ret = connect(fd,(struct sockaddr*)addr,len);
if (ret < 0 && errno == EINPROGRESS)
{
do{
ret = poll(fds,1,timeout);
}while(ret < 0 && errno == EINTR);
if (ret == 0) {
errno = ETIMEDOUT;
return -1;
} else if (ret < 0)
return -1;
else if (ret == 1) {
//两种可能,一种就是产生了错误,另外一种才是连接建立成功
int err;
socklen_t socklen = sizeof(err);
int sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&socklen);
if (sockoptret == -1){
return -1;
}
if (err == 0)
return 0;
else {
errno = err;
return -1;
}
}
}
return ret;
}
通常客户端连接服务器端的时候会选择一个随机的端口进行连接,但是某些程序要求客户端连接的时候必须是从某个指定端口开发起的连接。但是这样的客户端会存在一个问题,当客户端主动断开连接的时候会导致TIME_WAIT
状态,伺候2MSL
期间,这个客户端都无法再次发起连接,如果是使用随机断开则不会出现这个问题。
当内核的连接队列中没有任何已经就绪的连接的时候,connect就会被阻塞,如果对端在指定端口上没有server启动和监听,那么connect将会被拒绝,并返回错误信息,connection refused。
要要读取的数据大小未知的时候,你可以让buffer尽可能的大,你也可以在读取数据的过程中动态的扩充buffer的大小,malloc分配一个大的地址空间但是这不是真正的物理内存,只有等这段地址空间被写入数据了才会真正映射到物理内存上。所以你可以不用担心过度的申请内存,导致的资源浪费。
close()
只是关闭了你的socket接口,并不是socket本身,当close()
调用后,会导致socket的描述符减少,一旦为0就会触发内核开始发起四次挥手的过程,因为某些技术原因会导致socket在调用close()后仍然处于活动状态数分钟,这是很正常的情况,例如上文中提到的socket处于TIME_WAIT
状态,在这个状态下就会停留2MSL
时间。
有两种方法,第一种方法就是使用inetd
或xinetd
,第二种方法所有的工作都自己来实现。inetd
是一个超级服务器可以负责帮我们监听,处理网络数据,因此如果使用了inetd
那么我们的程序就不需要处理与网络相关的代码了,inetd
会负责将接收到的数据通过标准输入传递到后端的程序进行处理。inetd
也会接收来自于后端程序的标准错误和标准输出的内容。如果你选择自己来实现,那么下面是实现的主要代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>
volatile sig_atomic_t keep_going = 1;
void termination_handler(int signum);
int main()
{
.....
if(chdir(HOME_DIR))
{
fprintf(stderr,"%s‘:",HOME_DIR);
perror(NULL);
exit(1);
}
switch(fork())
{
case -1:
perror("fork()");
exit(3);
case 0:
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
if(setsid() == -1) {
exit(4);
}
break;
default:
return 0;
}
if(signal(SIGTERM,termination_handler) == SIG_IGN)
signal(SIGTERM,SIG_IGN);
signal(SIGINT,SIG_IGN);
signal(SIGHUP,SIG_IGN);
while(keep_going)
{
.....
}
return 0;
}
void termination_handler(int signum)
{
keep_going = 0;
signal(signum,termination_handler);
}
使用select,poll,epoll这样的IO服用机制即可,内核会负责帮我们监听这些描述符的事件变化,然后给我们通知。应用层依次处理这些描述符的事件变化即可。
这个选项告诉内核即使这个端口busy(TIME_WAIT状态)也要继续重用它。如果这个端口busy但是不是TIME_WAIT状态则会返回一个地址正在使用的错误。
SO_LINGER选项的含义:
SO_LINGER将决定系统如何处理残存在套接字发送队列中的数据,处理方式无非有两种: 丢弃或者将数据继续发送到对端,优雅的关闭链接
使用getpeername可以得到客户端的地址,代码如下:
int t;
int len;
struct sockaddr_in sin;
struct hostent *host;
len = sizeof sin;
if(getpeername(t,(struct sockaddr*)&sin,&len) < 0)
perror("getpeername")
else {
if((host = gethostbyaddr((char*)&sin.sin_addr,
sizeof sin.sin_addr,
AF_INET)) == NULL)
perror("gethostbyaddr");
else
printf("remote host is ‘%s‘\n",host->h_name);
}
SO_REUSEADDR
大家可能很熟悉,其用途就是可以重用处于TIME_WAIT
状态的socket。因为linux系统本身不允许同时绑定多个socket到相同的地址和端口那么对于一个处于TIME_WAIT
状态的socket来说,就无法再创建一个和这个socket具有相同地址和端口的socket了,如果启SO_REUSEADDR
可以避免这问题。SO_REUSEPORT
和SO_REUSEADDR
有些不同,SO_REUSEPORT
允许多个AF_INET
和AF_INET6
类型的socket绑定到同一个地址和端口,但是必须对每一个socket都要设置SO_REUSEPORT
选项,为了防止hijacking
,所有绑定相同地址的进程都必须是相同的EUID。这个选项可以用于TCP的socket,也可以用于UDP的socket。对于TCP的socket来说,这个选项可以允许accept实现多线程服务器的负载均衡,传统的负载均衡方式是主线程accept然后分发进行处理。对于UDP来说,使用这个选项可以为多进程(线程)提供更好的分发效果像比如传统的多进程从相同的socket接收数据包来说。
使用INADDR_ANY来给socket绑定地址,也可以使用ioctl的SIOCGIFCONF获取有效的网络结构,然后有选择的进行bind。
当你不需要确保包的顺序到达,不需要确保数据包必须达到对端的时候,那么UDP是一个很好的选择,如果你发现TCP太慢,你需要一个更快的方案你可以考虑使用UDP。
如果UDP socket是未连接的那么就无法使用send或write等系统调用发送数据,只能使用sendto来发送数据,如果使用了connect那么socket就被绑定了目标地址,此时就可以使用send和write等系统调用来发送数据。
当你给UDP使用了connect,那么使用read就只能读取,连接对端发送过来的数据,其他的发送者发过来的数据无法接受,但是最重要的是连接的UDP可以接收到ICMP的错误。
如果对端没有服务等待接收数据,那么对端会丢弃发送过来的数据包,并响应ICMP错误,但是这个ICMP错误是异步的,不是返回到对应的套接字上也就是说对于未连接的UDP socket来说,发生了错误是无法感知的,只有使用 connected的socket才会让异步产生的ICMP错误返回到对应的套接字上,第一次调用send的时候发生了如果发生了ICMP错误,那么下次再调用send就会返回错误了。
标签:
原文地址:http://blog.csdn.net/zhangyifei216/article/details/51228085