1 TCP协议
1.1 概念
TCP是一种面向连接的、可靠的协议,有点像打电话,双方拿起电话互通身份之后就建立了连接,然后说话就行了,这边说的话那边保证听得到,并且是按说话的顺序听到的,说完话挂机断开连接。也就是说TCP传输的双方需要首先建立连接,之后由TCP协议保证数据收发的可靠性,丢失的数据包自动重发,上层应用程序收到的总是可靠的数据流,通讯之后关闭连接。
1.2 TCP数据包格式
1.3 TCP3次握手过程
1.3.1 客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的段1
- 客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况
- 另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。
- mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
1.3.2 服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。
- 服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
1.3.3 客户必须再次回应服务器端一个ACK报文,这是报文段3。
- 客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三方握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。
在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection refused:
1.4 数据传输
- 客户端发出段4,包含从序号1001开始的20个字节数据。
- 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。
- 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。
在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。
1.5 TCP4次挥手
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
-
客户端发出段7,FIN位表示关闭连接的请求。
-
服务器发出段8,应答客户端的关闭连接请求。
-
服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
-
客户端发出段10,应答服务器的关闭连接请求。
建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。
2 案例
2.1 SERVER端
#include<sys/socket.h>
#include<arpa/inet.h>
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#define SERV_PORT 9527
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
// 定义监听套接字、通信套接字
int lfd = 0, cfd = 0;
int ret,i;
char buf[BUFSIZ],client_IP[1024];
// 定义服务器端、客户端地址结构
struct sockaddr_in serv_addr,clit_addr;
// 客户端地址结构长度
socklen_t clit_addr_len;
// 初始化服务器端地址结构
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 监听套接字初始化以及创建失败处理
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1) {
sys_err("socket error");
}
// 将套接字绑定IP+端口
bind(lfd,(struct sockaddr *)&serv_addr, sizeof(serv_addr));
// 设置lfd套接字用于监听以及可以连接的客户端套接字数量
listen(lfd,128);
// 客户端地址结构长度初始化
clit_addr_len = sizeof(clit_addr);
// 本服务器端目前只可同时对一个客户端进行通信
while(1){
// 设置服务端套接字为被动状态,返回一个新的套接字用于通信以及创建失败处理
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);
if(cfd == -1){
sys_err("accept error");
}
// 打印客户端ip以及端口
printf("client ip: %s port:%d\n", inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr,client_IP,sizeof(client_IP)),ntohs(clit_addr.sin_port));
while(1) {
// 接收客户端传来的数据
ret = read(cfd,buf,sizeof(buf));
if(ret == 0) {
printf("the link is disconneted!\n");
break;
}
// 将客户端数据传入标准输出流中
write(STDOUT_FILENO,buf,ret);
// 将传来的数据变大写并传回到客户端
for(i = 0; i < ret; i++)
buf[i] = toupper(buf[i]);
write(cfd,buf,ret);
}
}
// close(lfd);
// close(cfd);
return 0;
}
2.2 CLIENT端
客户端得知服务器端ip以及端口即可通信
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#define SERV_PORT 9527
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int cfd;
int conter = 10;
char buf[BUFSIZ];
//服务器地址结构
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
// 转换ip地址由点分制到二进制
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
// 初始化用于通信的套接字
cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1)
sys_err("socket error");
// 将客户端套接字与服务器端套接字连接
int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
if (ret != 0)
sys_err("connect err");
while (--conter) {
// 将数据通过套接字传输给服务器端
write(cfd, "hello\n", 6);
// 从套接字读取数据
ret = read(cfd, buf, sizeof(buf));
// 将数据输出至标准输出
write(STDOUT_FILENO, buf, ret);
sleep(1);
}
// 关闭客户端套接字
close(cfd);
return 0;
}
2.3 实现效果
- 运行服务器端,服务器先阻塞等待客户端请求
- 运行客户端,客户端发送连接请求
- 连接成功,服务器端输出客户端ip以及端口
- 服务器获取客户端传来的数据并将其打印到标准输出以及对其进行大写转换
- 服务器将处理好的数据发送给客户端
- 客户端获取数据并打印至标准输出
- 客户端关闭套接字
客户端:
服务器端: