基本TCP套接字编程

基本TCP套接字编程

connect函数

#include<sys/socket.h> 
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen) 
返回:若成功则为0,若出错则为-1

客户端调用connect前不必非得调用bind函数,因为内核会确定源IP地址,并选择一个临时端口作为源端口。
如果是TCP套接字,调用connect会激发TCP的三次握手,而且仅在连接建立成功或出错时才返回(默认阻塞)。出错的情况有:

  • TCP客户没有收到SYN分节的响应,则返回ETIMEOUT错误
  • 若对客户的响应的RST,则表明服务主机在我们指定的端口上没有进程等待与之连接(服务器可能没在运行)。客户一收到RST就马上返回ECONNREFUSED错误。
  • 若客户发出的SYN在某个路由器上引发了目的地不可达的ICMP错误。若在规定时间内仍未收到响应,则返回EHOSTUNREACH或ENETUNREACH错误。

现在的大多数主机一般是小端序,网络字节序是大端序。

listen函数

当用socket函数创建一个套接字时,它被假设为一个主动套接字,listen函数将其转换为一个被动套接字,指示内核应接受指向该套接字的连接请求。调用listen函数导致套接从CLOSED状态转换到LISTEN状态。

#include <sys/socket.h>
int listen(int sockfd, int backlog);
返回值:成功的时候返回0;出错的时候返回-1,并且设置errno.

backlog表示该套接字排队的最大连接个数。被定义为未完成连接队列和已完成连接队列总和的最大值。

  • 未完成连接队列。客户发送的SYN已到达服务端,但服务器正在等待完成相应的TCP三次握手过程。这些套接字处于SYN_RCVD状态。
  • 已完成连接队列。每个已完成TCP三次握手的套接字对应其中的一项。这些套接字处于ESTABLISHED状态。

基本TCP套接字编程

  • 如果三次握手正常完成,该套接字从未完成连接队列移到已完成队列的队尾。当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果说队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。
  • 当一个客户的SYN到达时,若队列是满的,TCP就忽略该SYN,而不会发送RST,因为客户在规定时间内会重发SYN。
  • 在三次握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排除,最大数据量为相应已连接套接字的接收缓冲区大小。

accept函数

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);
返回值:若成功则为非负描述符,若出错则为-1

由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。若队列为空,则进程被投入睡眠(默认阻塞)。
第一个参数为监听套接字,返回值为已连接套接字。

网络编程可能会遇到的三种情况:

  • fork子进程时,必须捕获SIGCHLD信号。(以免产生僵尸进程)
  • 当捕获信号时,必须处理被中断的系统调用。

当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误,表示系统调用被中断。并非所有的被中断系统调用都可以自动重启。因此我们需要自己重启被中断的系统调用。

for (;;){
       clilen = sizeof(cliaddr);
       if ((connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0) {
              if (errno = EINTR)
                     continue;
              else
                     err_sys("accept error");
       }
}

对于accept以及诸如read、write、select和open之类的函数来说,这是合适的。但connect函数不能重启。如果connect返回EINTR,我们就能再次调用它,否则将返回一个错误。当connect被一个捕捉的信号中断而且不自动重启时,这时已发起的三次握手会继续进行,但我们必须调用select来等待连接的完成。

  • SIGCHLD的信号处理函数应使用waitpid函数以免留下僵尸进程。(非阻塞waidpid)

    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {}

accept返回前连接中止

三次握手完成从而连接建立之后,客户TCP却发送了一个RST。在服务端看来,就在该连接已由TCP排队,等着服务器进程调用accept时RST到达。稍后,服务端调用accept。大多数实现返回一个错误给服务器进程,作为accept的返回结果,POSIX指出返回的errno值必须是ECONNABORTED,这时服务器会忽略它,再次调用accpet来处理其它连接。

close函数

每个文件或套接字都有一个引用计数,表示当前打开着的引用该文件或套接字的描述符的个数。close函数只是将相应的引用计数减1。因此,若已连接的套接字是在子进程中处理的,父进程对每个由accept返回的已连接套接字都应调用close,否则将耗尽文件描述符。
若我们确实想在某个TCP连接上发送一个FIN,我们应使用shutdown函数。

客户套接字的处理

  • 如果对方发送数据,那么套接字可读,并且read返回一个大于0的值(读入字节数);
  • 如果对方发送了FIN(对端进程终止),那么该套接字变为可读,并且read返回0(EOF);
  • 如果对方发送RST(对端主机崩溃并重启),那么该套接字变为可读,并且read返回-1,errno中含有确切错误码;

服务器进程终止

  • 服务器子进程终止,作为进程终止处理的前半部分,子进程中的所有打开着的描述符都被关闭。这就导致向客户发送了一个FIN,而客户则响应一个ACK。这就是TCP连接终止工作的前半部分。服务器子进程向父进程发送SIGCHLD信号。
  • 客户端中使用select监控套接字是否可读。此时服务端发送的FIN,使套接字变为可读,read返回0,客户进程返回信息(“str_cli:server terminated prematurely”)并终止,它所有打开的描述符都被关闭。
  • 若客户端没有使用诸如select的函数来监控套接字是否可读,在收到服务端的FIN后,客户端向socket发送数据,TCP服务端收到来自客户的数据时,既然先前打开那个套接字的服务器子进程已经终止,于是响应一个RST。当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送SIGPIPE信号,该信号默认是终止进程。此时写操作返回EPIPE错误,应采取措施终止进程。

void str_cli(FILE fp, int sockfd) {
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);
for ( ; ; ) {
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /
socket is readable /
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit(“str_cli: server terminated prematurely”);
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) { /
input is readable /
if (Fgets(sendline, MAXLINE, fp) == NULL)
return; /
all done */
Writen(sockfd, sendline, strlen(sendline));
}
} }

服务器主机崩溃

  • 当服务器主机崩溃时,已有的网络连接上不发出任何东西
  • 若客户端发送一个数据包,会发现客户TCP会持续重传数据分节,试图从服务器上接收一个ACK。若在规定时间内(约9分钟),服务器未重启,或者是服务器网络上不可达,此时客户端返回的错误会是ETIMEOUT或EHOSTUNREACH(ENETUNREACH)。如果我们想尽快检测出这种错误,我们可以为诸如recvfrom的函数设置超时。

int readable_timeo(int fd, int sec) {
fd_set rset;
struct timeval tv;
FD_ZERO(&rset);
FD_SET(fd, &rset);
tv.tv_sec = sec;
tv.tv_usec = 0;
return(select(fd+1, &rset, NULL, NULL, &tv));
/* 4> 0 if descriptor is readable */ }

void dg_cli(FILE *fp, int sockfd, const SA pservaddr, socklen_t
servlen) {
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
if (Readable_timeo(sockfd, 5) == 0) {
fprintf(stderr, “socket timeout\n”);
} else {
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = 0; /
null terminate */
Fputs(recvline, stdout);
}
} }

上述情况只有客户端向服务器主机发送数据时才能检测出它已崩溃,如果我们希望不主动发送数据也能检测出服务器主机的崩溃,需要设置SO_KEEPALIVE选项。

服务器主机崩溃后重启

此时在客户TCP重传数据分节时,若服务器主机崩溃后重启了,服务器的TCP已经丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应一个RST。当客户端收到该RST时,返回ECONNRESET错误。

服务器主机关机

Unix系统关机时,init进程通常先给所有进程发送SIGTERM信号(可被捕获,默认终止进程),然后等待一段固定的时间(5~20秒),然后给所有仍在运行的进程发送SIGKILL信号(不可被捕获)。这么做留给所有运行的进程一小段时间来清除和终止。这时发生的情况就与服务器进程终止的情况是一样的了。

相关文章: