对比IP协议,TCP协议是一种可靠的协议,它的可靠性最主要体现在对各种复杂错误的修正与处理。当数据报从一端通过网络发往另一端的过程中,时时刻刻都会遇到数据报被损坏、数据报重复、乱序等常见问题,
现在我们以一个基于WAN传输的系统为例,在应用程序之间进行通信的过程中可能遇到以下常见的中断:暂时或者永久的网络紊乱、对等方应用程序出现崩溃(进程挂掉)以及对等方应用程序运行的主机出现崩溃(电脑重启或者死机)。
网络紊乱:
在这种情况下,应用程序以及协议栈就不知道此时发生了紊乱,除非中间路由器利用ICMP消息告诉发送端目的主机或者网络不可达,在一些TCP实现上,会返回ENETUNREACH或者EHOSTUNREACH错误。
对等方应用程序崩溃(进程挂掉):
无论应用程序是如何结束的(close或者exit),对等方均会发送FIN消息给发送方,下面一段代码是用来模拟这种中断情况的:
/*********************************readn.h内容*****************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <netdb.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#define INIT() ( program_name = \
strrchr( argv[ 0 ], '/' ) ) ? \
program_name++ : \
( program_name = argv[ 0 ] )
#define EXIT(s) exit( s )
#define CLOSE(s) if ( close( s ) ) error( 1, errno, \
"close failed" )
#define set_errno(e) errno = ( e )
#define isvalidsock(s) ( ( s ) >= 0 )
#define NLISTEN 5 /* max waiting connections */
#define TRUE 1
#define FALSE 0
#define OPTIMIZE_READLINE
typedef unsigned int u_int32_t;
typedef int SOCKET;
/*-----------------Start Heartbeat include file--------------------*/
#define MSG_TYPE1 1 /* application specific msg */
#define MSG_TYPE2 2 /* another one */
#define MSG_HEARTBEAT 3 /* heartbeat message */
typedef struct /* message structure */
{
u_int32_t type; /* MSG_TYPE1, ... */
char data[ 2000 ];
} msg_t;
#define T1 60 /* idle time before heartbeat */
#define T2 10 /* init a heartbeat and timeout to wait for response */
/*-----------------Heartbeat include file--------------------*/
SOCKET tcp_server( char *hname, char *sname ); //setup tcp server
int readvrec( SOCKET fd, char *bp, size_t len );
int readn( SOCKET fd, char *bp, size_t len);
SOCKET tcp_client( char *hname, char *sname );
SOCKET udp_server( char *, char * );
SOCKET udp_client( char *, char *, struct sockaddr_in * );
void set_address( char *hname, char *sname, struct sockaddr_in *sap, char *protocol );
#ifndef OPTIMIZE_READLINE
int readline(int fd, void *vptr, size_t maxlen);
#else
int readline( int fd, char *bufptr, size_t maxlen );
#endif
/******************************************readn.c内容**************************/
#include "readn.h"
//setup for a tcp server
SOCKET tcp_server( char *hname, char *sname )
{
struct sockaddr_in local;
SOCKET s;
const int on = 1;
set_address( hname, sname, &local, "tcp" );
s = socket( AF_INET, SOCK_STREAM, 0 );
if ( !isvalidsock( s ) )
{
error( 1, errno, "socket call failed" );
}
if ( setsockopt( s, SOL_SOCKET, SO_REUSEADDR, ( char * )&on, sizeof( on ) ) )
{
error( 1, errno, "setsockopt failed" );
}
if ( bind( s, ( struct sockaddr * ) &local, sizeof( local ) ) )
{
error( 1, errno, "bind failed" );
}
if ( listen( s, NLISTEN ) )
{
error( 1, errno, "listen failed" );
}
return s;
}
/* readn - read exactly n bytes */
int readn( SOCKET fd, char *bp, size_t len)
{
int cnt;
int rc;
int var;
/*dragon, setup NO BLOCK option, and maybe cause error about "Resource temporarily unavailable",
*so should exclude errno is EAGAIN
*/
// var = fcntl(fd, F_GETFL, 0);
// fcntl(fd, F_SETFL, var|O_NONBLOCK);
cnt = len;
while ( cnt > 0 )
{
//dragon, default option of recv is block
// rc = recv( fd, bp, cnt, 0 );
rc = recv( fd, bp, cnt, MSG_DONTWAIT); //NON_BLOCK operation
if ( rc < 0 ) /* read error? */
{
if ( errno == EINTR || errno == EAGAIN) /* interrupted? */
continue; /* restart the read */
return -1; /* return error */
}
if ( rc == 0 ) /* EOF? */
{
return len - cnt; /* return short count */
}
bp += rc;
cnt -= rc;
}
return len;
}
/* readvrec - read a variable record */
int readvrec( SOCKET fd, char *bp, size_t len )
{
u_int32_t reclen;
int rc = 0;
/* Retrieve the length of the record */
rc = readn( fd, ( char * )&reclen, sizeof( u_int32_t ) );
if ( rc != sizeof( u_int32_t ) )
{
return rc < 0 ? -1 : 0;
}
reclen = ntohl( reclen );
if ( reclen > len )
{
/*
* Not enough room for the record--
* discard it and return an error.
* Need to discard data in the receive buffer.
*/
//How to discard receive buffer
while ( reclen > 0 )
{
rc = readn( fd, bp, len );
if ( rc != len )
return rc < 0 ? -1 : 0;
reclen -= len;
if ( reclen < len )
len = reclen;
}
return -1;
}
/* Retrieve the record itself */
rc = readn( fd, bp, reclen );
if ( rc != reclen )
{
return rc < 0 ? -1 : 0;
}
return rc;
}
/*-------------------------Before optimize-------------------------*/
//PS: each read just only read one character, we can read more characters and store it to buffer, such as array,
// that is trading time for space.
#ifndef OPTIMIZE_READLINE
int readline(int fd, void *vptr, size_t maxlen)
{
int n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = read(fd, &c, 1)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
if (n == 1)
return(0); /* EOF, no data read */
else
break; /* EOF, some data was read */
} else
return(-1); /* error */
}
*ptr = 0;
return(n);
}
#else
int readline( int fd, char *bufptr, size_t len )
{
char *bufx = bufptr;
static char *bp;
static int cnt = 0;
static char b[ 1500 ];
char c;
while ( --len > 0 ) //should first plus to add an extra char buf to store ending character
{
if ( --cnt <= 0 )
{
cnt = recv( fd, b, sizeof( b ), 0 );
if ( cnt < 0 )
{
if ( errno == EINTR )
{
len++; /* the while will decrement */
continue;
}
return -1;
}
if ( cnt == 0 )
{
return 0;
}
bp = b;
}
c = *bp++;
*bufptr++ = c;
if ( c == '\n' )
{
*bufptr = '\0';
return bufptr - bufx;
}
}
set_errno( EMSGSIZE ); //return over size error
return -1;
}
#endif
/*-------------------------End optimize-------------------------*/
/* tcp_client - set up for a TCP client */
SOCKET tcp_client( char *hname, char *sname )
{
struct sockaddr_in peer;
SOCKET s;
set_address( hname, sname, &peer, "tcp" );
s = socket( AF_INET, SOCK_STREAM, 0 );
if ( !isvalidsock( s ) )
{
error( 1, errno, "socket call failed" );
}
if ( connect( s, ( struct sockaddr * )&peer, sizeof( peer ) ) )
{
error( 1, errno, "connect failed" );
}
return s;
}
/* set_address - fill in a sockaddr_in structure */
void set_address( char *hname, char *sname, struct sockaddr_in *sap, char *protocol )
{
struct servent *sp;
struct hostent *hp;
char *endptr;
short port;
bzero( sap, sizeof( *sap ) );
sap->sin_family = AF_INET;
if ( hname != NULL )
{
if ( !inet_aton( hname, &sap->sin_addr ) )
{
hp = gethostbyname( hname );
if ( hp == NULL )
error( 1, 0, "unknown host: %s\n", hname );
sap->sin_addr = *( struct in_addr * )hp->h_addr;
}
}
else
sap->sin_addr.s_addr = htonl( INADDR_ANY );
port = strtol( sname, &endptr, 0 );
if ( *endptr == '\0' )
sap->sin_port = htons( port );
else
{
sp = getservbyname( sname, protocol );
if ( sp == NULL )
error( 1, 0, "unknown service: %s\n", sname );
sap->sin_port = sp->s_port;
}
}
//setup an udp server
SOCKET udp_server( char *hname, char *sname )
{
SOCKET s;
struct sockaddr_in local;
set_address( hname, sname, &local, "udp" );
s = socket( AF_INET, SOCK_DGRAM, 0 );
if ( !isvalidsock( s ) )
{
error( 1, errno, "socket call failed" );
}
if ( bind( s, ( struct sockaddr * ) &local, sizeof( local ) ) )
{
error( 1, errno, "bind failed" );
}
return s;
}
/******************************************* count_client.c******************************/
#include "readn.h"
static char *program_name = NULL;
static void error( int status, int err, char *fmt, ... )
{
va_list ap;
va_start( ap, fmt );
fprintf( stderr, "%s: ", program_name );
vfprintf( stderr, fmt, ap );
va_end( ap );
if ( err )
{
fprintf( stderr, ": %s (%d)\n", strerror( err ), err );
}
if ( status )
{
EXIT( status );
}
}
int main( int argc, char **argv )
{
SOCKET s;
int rc = 0;
int len = 0;
char buf[ 120 ] = {0};
INIT();
s = tcp_client( argv[ 1 ], argv[ 2 ] );
while ( fgets( buf, sizeof( buf ), stdin ) != NULL )
{
len = strlen( buf );
rc = send( s, buf, len, 0 );
if ( rc < 0 )
{
error( 1, errno, "send failed" );
}
rc = readline( s, buf, sizeof( buf ) );
if ( rc < 0 )
{
error( 1, errno, "readline failed" );
}
else if ( rc == 0 )
{
error( 1, 0, "server terminated\n" );
}
else
{
fputs( buf, stdout );
}
}
EXIT( 0 );
}
/********************************* count _server.c******************************/
#include "readn.h"
static char *program_name = NULL;
static void error( int status, int err, char *fmt, ... )
{
va_list ap;
va_start( ap, fmt );
fprintf( stderr, "%s: ", program_name );
vfprintf( stderr, fmt, ap );
va_end( ap );
if ( err )
fprintf( stderr, ": %s (%d)\n", strerror( err ), err );
if ( status )
EXIT( status );
}
int main( int argc, char **argv )
{
SOCKET s;
SOCKET s1;
int rc;
int len;
int counter = 1;
char buf[ 120 ];
INIT();
s = tcp_server( NULL, argv[ 1 ] );
s1 = accept( s, NULL, NULL );
if ( !isvalidsock( s1 ) )
{
error( 1, errno, "accept failed" );
}
while ( ( rc = readline( s1, buf, sizeof( buf ) ) ) > 0 )
{
sleep( 5 );
len = sprintf( buf, "received message %d\n", counter++ );
rc = send( s1, buf, len, 0 );
if ( rc < 0 )
{
error( 1, errno, "send failed" );
}
}
EXIT( 0 );
}
相应的Makefile文件如下:
all: count_client count_server
CC = gcc
test_server:
$(CC) count_server.c readn.c -o count _server
test_client:
$(CC) count _client.c readn.c -o count_client
clean:
rm -rf count _server count _client *.o
以上代码逻辑非常清晰,在TCP客户端与服务器成功建立连接之后,然后客户端接收标准输入中内容,并将其发送给服务器端,服务器在收到内容之后,等待5s之后返回结果。现在我们试图模拟两种情况,那就是在5s之前与之后Kill掉服务器进程,并进行对比。
首先我们先观察在5s之后客户端成功收到返回结果之后,将服务器端kill掉之后,客户端立即失败,那是因为服务器被kill掉之后,会给客户端发送FIN信号,但是客户端应用程序在接收到FIN时发送出”45454541”是完全合理的,因为发送FIN信号仅仅只是代表它没有数据再发送。此时连接已经不存在,会返回一个RST信号,整个连接终结。整个过程如以下示意图:
现在我们考虑下在5s之前就将服务器端kill掉的情况,如下图,当server端被kill掉之后(CTRL+C),此时客户端立马返回错误,而不是同之前的需要等待下一次输入之后才知道连接已经终结,二者的主要差别在于前一种情况是阻塞于客户端的fgets函数,而后者是阻塞在readline函数。
整个过程如下图:
对等方主机崩溃:
这种情况与应用程序崩溃又是存在区别的,因为后者可以通过发送FIN信号通知对方,而主机崩溃这种情况,只有等到主机重新启动才知道,因此,这种错误有点类似于网络紊乱。现在我们来考虑这样一种对等方主机是在本方TCP准备撤销连接(肯定是未达到重传的最大次数)之前启动的,那二者之间是如何交互的呢?当本方的重传段被对等方接收,但是对等方主机并没有相关的连接记录(重启之后初始化),TCP规范要求应该给发送方发送RST信号,这样会导致本方主动撤销连接,此时若本方应用程序中存在读等待,则会返回ECONNRESET错误,而对于下一次读操作,则会导致SIGPIPE或者EPIPE错误。