连接
TCP/IP协议规定网络数据传输应采用大端字节序
- socket地址
struct sockaddr{
unsigned short sa_family;
char sa_data[14];
};
一般不采用上述socket地址,系统兼容性考虑采用sockaddr_in。
#include <netinet/in.h>
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr;
};
struct sockaddr_in {
sa_family_t sin_family; //short
in_port_t sin_port; // unsigned short
struct in_addr sin_addr;
};
- socket
sys/socket.h
int socket(int domain, int type, int protocal);
family: AF_INET, AT_INET6, AF_UNIX
AF_UNIX只能用于单一的(unix)进程间通信
AF_INET是针对Internet的,允许在远程主机之间通信
type: SOCK_STREAM(tcp), SOCK_DGRAM(udp)
protocal:协议 0
return:失败-1,成功fd。
- bind
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
INADDR_ANY标识可以和任何的主机通信。
return: 失败-1(设置errno),成功0
- listen
int listen(int sockfd, int backlog);
backlog:最多允许有多上个客户端处于连接等待状态,超过(backlog+1)忽略。此处的backlog限定的是同时建立连接(accept)的数据(已经accept的不包含)。
由于TCP使用的3次握手,连接在到达ESTABLISHED
状态之前经历中间状态SYN RECEIVED
,并且可以由accept
系统调用返回到应用程序。
这意味着TCP / IP
堆栈有两个选择来为LISTEN
状态的套接字实现backlog
队列:一种就是两种状态在一个队列,一种是分别在一个队列。
linux使用两个队列
实现,一个SYN
队列(或半连接队列)和一个accept
队列(或完整的连接队列)。 处于SYN RECEIVED
状态的连接被添加到SYN
队列,并且当它们的状态改变为ESTABLISHED
时,即当接收到3次握手中的ACK
分组时,将它们移动到accept
队列。 显而易见,accept
系统调用只是简单地从完成队列中取出连接。 在这种情况下,listen syscall
的backlog参数表示完成队列
的大小。
如在listen
系统调用的手册中所提到的:
在Linux内核2.2之后,socket backlog
参数的形为改变了,现在它指等待accept
的完全建立
的套接字的队列长度,而不是不完全连接请求的数量。
不完全连接的长度可以使用/proc/sys/net/ipv4/tcp_max_syn_backlog
设置。
参考:深入探索 Linux listen() 函数 backlog 的含义
return:失败-1,成功0
listen完成后,内核自动完成三次握手,与accept无关。
当有客户端发起链接时,服务器端调用accept()返回并接受这个连接。如果有大量客户端发起连接,服务器来不及处理,尚未accept的客户端就处于连接等待状态。
- accept
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
cliaddr:客户端地址(传出);
addrlen:传入传出(缓冲区大小,地址大小)
return:失败-1,成功:新的文件描述符。
服务器端调用accept()还没有客户端连接请求时,阻塞等待。
链接关闭时,返回错误码ECONNABORTED,此时应重试连接。
perror("accept()"); if((errno == ECONNABORTED) || (errno == EINTR)) continue;
- connect
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
servaddr:服务器地址。
return:失败-1;成功0。
客户端port由内核自动分配。
当连接不成功时,应阻塞等待。
出错原因:
- 指定IP无效。
- 无服务器进程。
- 超时Timeout。
慢系统调用accept,read,write被信号中断时应该重试。对于accept,如果errno为ECONNABORTED,也应该重试。
connect()成功并不表示与服务器连接成功:
应该注意到客户端认为的连接状态是成功建立的(向服务器发送了ack),因为从客户端端口到服务器端口的连接都是处于ESTABLISHED的。这将导致客户端程序执行connect时是成功返回的,并继续下一步的动作,向服务器发送数据。
由于数据的发送是交给系统底层完成的,当客户端执行send时,数据将传送给系统底层。如果系统底层可以接收所有数据,则客户程序认为发送成功并返回。这点通过客户端执行结果,可以得到验证。所有客户端都是立即完成,并输出以下信息:
connect successfully.
sent byte: 1024
实际的情况是什么样的呢?通过命令“tcpdump -i lo”观察数据的传送。从以下数据可以看出,服务器程序实际上并没有与客户端程序建立连接,而且数据传输也没有真正完成。
总结:客户端connect成功返回时,并不表示与服务端的连接已经真正建立。Send发送数据成功返回也不表示服务器端已经成功接收了。编程时应该注意到这两点。
On the other hand, if the client first waits for data from the server and the server never reduces the backlog, then the end result is that on the client side, the connection is in state ESTABLISHED, while on the server side, the connection is considered CLOSED. This means that we end up with a half-open connection!
TCP确实有对数据的确认,因此当一方向另一方发送数据时,如果连接仍然处于活动状态,它将收到一个确认(否则会收到一个错误)。因此,可以通过发送数据来检测断开的连接。需要注意的是,在TCP中,接收数据的行为是完全被动的;只读取的套接字无法检测到断开的连接。
由于断开的连接只能通过发送数据检测到,所以接收端将永远等待。这种情况被称为“半开放连接”,因为一方意识到连接已丢失,但另一方认为它仍处于活动状态。
如何处理?
熟悉套接字通用选项的朋友一定已经有了想法。TCP套接字不是有个保持存活选项SO_KEEPALIVE嘛,如果在两个小时之内在该套接字的任何一个方向上都没数据交换,TCP就自动给对端发送一个保持存活探测分节,如果此TCP探测分节的响应为RST,说明对端已经崩溃且已经重新启动,该套接字的待处理错误被置为ECONNRESET,套接字本身则被关闭。如果没有对此TCP探测分节的任何响应,该套接字的处理错误就被置为ETIMEOUT,套接字本身则被关闭。
确实,这个选项确实可以处理我们前面遇到的TCP半开连接的问题,但是默认两小时间隔探测的实时性是不是差了些呢?当然,我们可以通过修改内核参数改小时间间隔,完美了吧?但是必须注意的是大多数内核是基于整个内核维护这些时间参数的,而不是基于每个套接字维护的,因此如果把无活动周期从两小时改为(比如)2分钟,那将影响到该主机上所有开启了此选项的套接字。我想大家都不会愿意承担服务器端的这种不确定性吧。另外,心跳除了说明应用程序还活着(进程存在,网络畅通),更重要的是表明应用程序能正常工作。而SO_KEEPALIVE由操作系统负责探查,即便是进程死锁或有其他异常,操作系统也会正常收发TCP keepalive消息,而对方无法得知这一异常。
没关系,其实我们可以在应用层模拟SO_KEEPALIVE的方式,用心跳包来模拟保活探测分节。由于服务器通常要承担成千上万的并发连接,所以肯定是由客户端在应用层进行心跳来模拟保活探测分节,客户端多次收不到服务器的响应时可终止此TCP连接,而服务端可监测客户端的心跳包,若在一定时间间隔内未收到任何来自客户端的心跳包则可以终止此TCP连接,这样就有效避免了TCP半开连接的情况。
读写数据
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
return:成功,返回读取到的字节数;失败-1。
有数据到达,返回读到的字节数;无数据到达,
- 连接关闭,返回0
- 连接未关闭,阻塞。
sszie_t write(int fd, const void *buf, size_t count);
return:成功,返回写入的字节数;失败-1。
连接关闭,收到SIGPIPE,返回-1。连接未关闭,
- 发生流量控制,阻塞;
- 无流量控制,返回写入字节数。
关闭socket
关闭socket有两个函数close和shutdown。
int shutdown(int sockfd, int howto);
TCP连接是双向的(可读可写),当我们使用close时,会把读写通道都关闭,有时候希望只关闭一个方向,这个时候用shutdown。
howto=0关闭读通道,可以写。
howto=1关别写通道,可以读。
howto=2关闭读写通道,和close一样。
注:shutdown不能代替close,其仅关闭连接,不销毁连接套接字,其后还要close。
多进程程序里,如果有几个进程共享一个socket,如果使用shutdown,那么所有子进程都不能操作了,这个时候只能使用close来关闭子进程的套接子描述符。
总结
总的来说,网络程序是由两个部分组成的--客户端和服务器端。它们建立的步骤一般是:
服务器端
socket-> bind -> listen -> accept
客户端
socket -> connect
注:由于客户端不需要固定端口,因此不必调用bind()。不调用bind(),端口号由内核自动分配。
客户端不是不可以调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),由内核分配端口号,每次重启服务器时端口号都不一样,客户端连接服务器就会遇到麻烦。
示例
客户端程序读数据,服务器端多进程并发写数据。
每次accept一个新客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程。
// ser.c #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <errno.h> #include <time.h> #define SERV_PORT 10000 #define MAX_CONN 2 #define STR_SND "Time: " int main(int argc, char *argv[]) { int fd, sfd; int ret = 0; struct sockaddr_in saddr, cliaddr; socklen_t slen; fd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == fd){ fprintf(stderr, "socket error "); exit(EXIT_FAILURE); } int opt = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); memset(&saddr, 0, sizeof(struct sockaddr_in)); saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = htonl(INADDR_ANY); saddr.sin_port = htons(SERV_PORT); ret = bind(fd, (struct sockaddr *)&saddr, sizeof(struct sockaddr)); if(-1 == ret){ fprintf(stderr, "bind error "); exit(EXIT_FAILURE); } ret = listen(fd, MAX_CONN); if( -1 == ret){ fprintf(stderr, "listen error "); exit(EXIT_FAILURE); } signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); int len = strlen(STR_SND); char buf[64], buftime[64]; strcpy(buf, STR_SND); while(1){ sleep(10); slen = sizeof(struct sockaddr); sfd = accept(fd, (struct sockaddr*)&cliaddr, &slen); if(sfd < 0){ // fprintf(stderr, "accept error "); perror("accept()"); if((errno == ECONNABORTED) || (errno == EINTR)){ continue; } else { exit(EXIT_FAILURE); } } ret = fork(); if(ret > 0){ // parent printf("[%d:%d] listen... ", getppid(), getpid()); continue; } else if (ret == 0){ //child printf("connet:%s %d ", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); while(1){ //ret = write(sfd, STR_SND, sizeof(STR_SND)); //ret = write(sfd, STR_SND, 100); sprintf(buftime, "%ld", time(NULL)); strcpy(buf+len, buftime); ret = write(sfd, buf, strlen(buf)); if(ret == -1){ if(errno == EPIPE){ close(sfd); printf("[%d:%d] client [%s:%d] disconnect ", getppid(), getpid(), inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); //exit(EXIT_SUCCESS); return -1; } } printf("[%d:%d] send client [%s:%d](%ld): %s ", getppid(), getpid(), inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), strlen(buf), buf); sleep(3); } } else if(ret < 0) { perror("fork"); fprintf(stderr, "fork error "); } } close(fd); return 0; }
// cli.c #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #define SERV_IP "127.0.0.1" #define SERV_PORT 10000 #define BSIZE 1024 int main(int argc, char *argv[]) { int fd; int ret = 0; struct sockaddr_in saddr; char buf[BSIZE]; fd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == fd){ fprintf(stderr, "socket error "); exit(EXIT_FAILURE); } memset(&saddr, 0, sizeof(struct sockaddr_in)); saddr.sin_family = AF_INET; inet_pton(AF_INET, SERV_IP, &saddr.sin_addr.s_addr); saddr.sin_port = htons(SERV_PORT); ret = connect(fd, (struct sockaddr *)&saddr, sizeof(struct sockaddr)); if( -1 == ret){ fprintf(stderr, "connect error "); exit(EXIT_FAILURE); } while(1){ memset(buf, 0, sizeof(buf)); ret = read(fd, buf, sizeof(buf)); if( ret > 0){ if(buf[ret-1] != ' '){ buf[ret-1] = ' '; } fprintf(stdout, "%d:%s", ret, buf); fflush(stdout); } else if(ret == 0){ // network disconneted fprintf(stderr, "disconnect "); break; } else { if(errno == EINTR){ continue; } } } close(fd); return 0; }
运行结果:
~$gcc ser.c -Wall -o ser ~$gcc cli.c -Wall -o cli ~$./ser connet:127.0.0.1 43731 connet:127.0.0.1 43732 connet:127.0.0.1 43733 connet:127.0.0.1 43734 connet:127.0.0.1 43735 connet:127.0.0.1 43736 client [127.0.0.1:43736] disconnect client [127.0.0.1:43733] disconnect client [127.0.0.1:43734] disconnect client [127.0.0.1:43732] disconnect client [127.0.0.1:43735] disconnect client [127.0.0.1:43731] disconnect ~$./ser bind error ~$ps aux | grep -w 'ser' yuxi 31890 0.0 0.0 2028 60 pts/1 S 23:22 0:00 ./ser yuxi 31894 0.0 0.0 6156 876 pts/1 S+ 23:22 0:00 grep --color=auto -w ser ~$killall ser ~$ps aux | grep -w 'ser' yuxi 31897 0.0 0.0 6156 872 pts/1 S+ 23:22 0:00 grep --color=auto -w ser
"bind error"是因为父进程一直在监听,没有退出。
从上述示例也可以看出,判断网络断开的方法:read返回sockfd,返回0;write返回-1,errno为EPIPE。
注:SIGPIPE默认动作为程序退出,一旦收到SIGPIPE信号程序就退出,从而无法判断errno,故设置signal(SIGPIPE, SIG_IGN);
对一个对端已经关闭的socket调用两次write, 第二次将会生成SIGPIPE信号, 该信号默认结束进程.
具体的分析可以结合TCP的"四次握手"关闭. TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端的socket是调用了close还是shutdown.
对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.
参考:http://www.360doc.com/content/11/0604/09/4363353_121584610.shtml
可参考:
1. linux网络编程之socket(四):使用fork并发处理多个client的请求和对等通信p2p