运行环境:CentOS6.5
简单 的Linux服务器:
#include
#include
#include
#include
#include
#include
#include
#include
void serviceIO(int fd){
while(1){
char buf[1024];
ssize_t len =read(fd,buf,sizeof(buf));
if(len>0){
buf[len]=0;
printf("client#%s\n",buf);
}
else if(len==0){
printf("link is broken\n");
break;
}
else{
perror("read");
break;
}
}
}
int main(int argc,char *argv[]){
if(argc!=3){
printf("%s\n",argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
perror("sock");
return 2;
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(argv[1]);
local.sin_port = htons(atoi(argv[2]));
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
return 3;
}
if(listen(sock,10)<0){
perror("listen");
return 4;
}
while(1){
struct sockaddr_in perr;
socklen_t len = sizeof(perr);
int fd = accept(sock,(struct sockaddr *)&perr,&len);
if(fd<0){
perror("accept");
continue;
}
printf("get a new link...\n");
serviceIO(fd);
close(fd);
}
return 0;
}
可以看到这个程序的功能很简单,获取TCP客户端的连接,打印来自客服端的消息。
简单的Linux客服端程序:
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc ,char *argv[]){
if(argc!=3){
printf("arguments error\n");
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
perror("sock");
return 2;
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(argv[1]);
local.sin_port = htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr *)&local,sizeof(local))<0){
perror("connect");
return 3;
}
while(1){
char buf[1024];
printf("please enter#\n");
scanf("%s",buf);
write(sock,buf,strlen(buf));
}
close(sock);
}
客户端程序的作用时:与服务器简历连接并向服务器发送消息
(此处不暂对程序各部分功能的具体实现作介绍)
TCP是面向链接的服务,因此在通信之前首先得建立链接,建立链接的过程称为“三次握手”。
服务器:
首先调用bind方法绑定套接字和IP,端口号
【CLOSED -> LISTEN】 调用listen方法使服务器进入监听状态。
客服端:
【CLOSED -> SYN_SENT】调用connect方法,发送请求连接(带有SYN信号的同步报文段)给服务器,并阻塞等待服务器响应。 第一次握手
服务器:
【LISTEN -> SYN_RCVD】服务器监听到链接请求(同步报文段),将该链接放入内核等待队列,并向客户端发送SYN确认报文(SYN + ACK)。 第二次握手
客户端:
【SYN_SENT -> ESTABLBLISHED】 客户端收到服务器发来的SYN确认报文,即认为链接成功,并向服务器发送ACK报文。 第三次握手
服务器:
【SYN_RCVD -> ESTABLISHED】 服务器接到ACK确实则表示服务器认为链接成功。accept返回分配新的描述符connect和客户端通信。
至此三次握手完成。
三次握手是一个确定双方建立通信的过程,那么为什么是三次呢?
假如只有一次,那么只有客服端发出请求,他无法确定对方是否收到请求,对方也没有和他链接。
如果是两次,那么服务器就无法确定客户端是否收到了同步确认报文。
如果是四次,三次已经可以建立双方通信就没有必要,造成浪费。
在三次握手的情况下:
第一次握手失败,即服务器没有收到请求,则客服端在没有收到确认报文之后自然会再次请求连接。
第二次握手失败,客服端在没有收到同步确认报文,则不会向服务器返回确认报文,则双方认为链接失败,客户端再次请求,服务器没有收到确认报文就会断开之前的连接。
第三次握手失败,客户端认为链接成功,但是服务器没有收到来自客服端的确实报文,服务器会断开链接。那么此时由客服端维护着一个无效的链接。这也是没有第四次握手的原因,因为如果是四次握手的话,可能会再把这个无效链接扔给服务器,占用服务器的宝贵资源。
假如是客服端断开链接:
客服端:
[ESTABLISHED -> FIN_WAIT_1] 客户端主动调⽤用close时, 向服务器发送结束报⽂文段, 同时进⼊入 FIN_WAIT_1;
服务器:
[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调⽤用close), 服务器会收到结束报⽂文段, 服务器返回确认报⽂文段并进入CLOSE_WAIT; 第一次挥手
客户端:
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报⽂文段的确认, 则进⼊入FIN_WAIT_2, 开始 等待服务器的结束报⽂文段; 第二次挥手
服务器:
[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调⽤用close关闭连接时, 会向客户端发送FIN, 此时服务器进⼊入LAST_ACK状态, 等待最后⼀一个ACK到来(这个ACK是客户端确认收到了FIN) 第三次挥手
客服端:
[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报⽂文段, 进⼊入TIME_WAIT, 并发出 LAST_ACK; [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进⼊入CLOSED状态. 第四次挥手
服务器:
[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.
至此就完成了四次挥手,假如是服务器主动断开链接与这个过程相似。
主动断开链接的一方会进入TIME_WAIT状态,在这个时间段这个服务不能再次启动。且TIME_WAIT的时间是2MSL(MSL -> 报文的最大生存时间)。
MSL是报文的最大生存时间,TIME_WAIT是2MSL的话:
a.就能保证在两个传输方向上的尚未被接收或迟到的报⽂文段都已经消失(假如服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
b.同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个 FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset. 即使没有写入操作, TCP自己也内置了⼀一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外, 应⽤用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态。例如QQ, 在QQ断线之后, 也会定期尝试重新连接.