TCP 是 TCP/IP 协议族中面向连接的可靠协议,本文将介绍其工作流程以及在 Linux 中对其进行编程的方法。
TCP基础
TCP协议
- TCP全称为 “传输控制协议(Transmission Control Protocol”)。要对数据的传输进行一个详细的控制。
- TCP 向相邻的高层提供服务。 TCP 的上一层是应用层,因此,TCP 数据传输实现了从一个应用程序到另一个应用程序的数据传递。
- 应用程序通过编程调用 TCP 并使用 TCP 服务,提供需要准备发送的数据,用来区分接收数据应用的目的地址和端口号。
- 通常情况下, 应用程序通过打开一个 socket 来使用 TCP 服务,TCP 管理到其他 socket 的数据传递。
- 通过 IP 的源/目的可以唯一地区分网络中两个设备的连接, 通过 socket 的源/目的可以唯一地区分网络中两个应用程序的连接。
TCP 对话通过三次握手来进行初始化。
三次握手的目的是使数据段的发送和接收同步,告诉其他主机其一次可接收的数据量,并建立虚连接。
三次握手的简单过程:
- 初始化主机通过一个同步标志置位的数据段发出会话请求。
- 接收主机通过发回具有以下项目的数据段表示回复: 同步标志置位、即将发送数据段的起始宇节的顺序号、应答并带有将收到的下一个数据段的字节顺序号。
- 请求主机再回送一个教据段,并带有确认顺序号和确认号。
TCP 的三次握手过程示意
TCP 实体所采用的基本协议是滑动窗口协议,当发送方传送一个数据报时, 它将启动计时器。
当该数据报到达目的地后,接收方的 TCP 实体往回发送一个数据报,其中包含一个确认序号,表示希望收到的下一个数据包的顺序号。
如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据包。
TCP 的数据包头格式
对其各个部分说明如下:
- 源端口,目的端口:16位长,标识出远端和本地的端口号,表示数据是从哪个进程来, 到哪个进程去。
- 顺序号:32位长,标识发送数据报的顺序。
- 确认号:32位长,希望收到的下一个数据包的序列号。
- TCP头长:4位长,表明 TCP 头中包含多少个 32 位字(有多少个4字节),所以TCP头部最大长度是15 * 4 = 60。
- 6 位未用。
- URG:紧急指针是否有效。
- ACK:确认号是否有效。ACK位置 1 表明确认号是合法的,如果 ACK 为0,那么教据报不包含确认信息,确认字段被省略。
- PSH:表示是带有 PUSH 标志的数据,接收方只等请求数据包一到便将其送往应用程序而不必等到缓冲区装满时才传送。提示接收端应用程序立刻从TCP缓冲区把数据读走。
- RST:用于复位,主机崩溃或其他原因而出现的错误连接,对方要求重新建立连接,还可以用于拒绝非法的数据包或拒绝连接请求。把携带RST标识的称为复位报文段。
- SYN:用于建立连接
- FIN:用于释放连接。通知对方, 本端要关闭了, 把携带 FIN 标识的称为结束报文段。
- 窗口大小:16位长,窗口大小字段表示在确认了字节之后还可以发送多个字节。
- 校验和:16位长,是为了确保高可靠性而设置的,用于校验头部、数据和伪 TCP 头部之和。由发送端填充,CRC校验,接收端校验不通过,则认为数据有问题。此处的检验和包含TCP首部和TCP数据部分。
- 16位紧急指针:标识哪部分数据是紧急数据。
- 可选项:0个或多个32位字,包括最大 TCP 载荷、滑动窗口比例以及选择重发数据包等选项。
TCP 的工作流程
基于 TCP 传输协议的服务器与客户端间的通信工作流程可以利用下图所示的过程来描述。
- 服务器先用 socket 函数来建立一个套接口,用这个套接口完成通信的监听及数据的收发。
- 服务器利用 bind 函数来绑定一个端口号和 IP 地址,使套接口与指定的端口号、IP 地址相关联。
- 服务器调用 listen 函数, 使服务器的这个端口和 IP 处于监听状态,等待网络中某一客户机的连接请求。
- 客户机用 socket 函数建立一个套接口,设定远程 IP 和端口。
- 客户机调用 connect 函数连接远程计算机指定的端口。
- 服务器调用 accept 函数来接收远程计算机的连接请求,建立起与客户机之间的通信连接。
- 建立连接以后,客户机利用 write 函数或 send 函数向 socket 中写入数据,也可以使用 read 函数或 recv 函数读取服务器发送来的数据。
- 服务器利用 read 函数或 recv 函数读取客户机发送来的数据,也可以利用 write 函数或 send 函数来发送数据。
- 完成通信以后,使用 close 函数关闭 socket 连接。
应用实例
例1和例2是一个使用TCP进行通信的服务器端和客户端应用实例:服务器端接收客户端请求,创建一个子进程来向客户端发送当前系统时间;客户端则读取服务器端发送的信息。
【例1】TCP通信程序服务器端
应用代码使用端口 25555 作为通信端口,首先调用 socket 函数和 bind 函数建立套接字并且绑定端口,然后调用 listen 函数等待客户端连接,如果有客户端的连接信息,则使用 accept 函数接收该连接并且创建一个子进程, 在子进程中发送当前的时间信息,最后在子进程退出的时候关闭该套接字接口。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define SERV_PORT 25555 //服务器接听端口号
#define BACKLOG 20 //请求队列中允许请求数
#define BUF_SIZE 256 //缓冲区大小
int main(int arge,char *argv[])
{
int ret;
time_t tt;
struct tm *ttm;
char buf[BUF_SIZE];
pid_t pid; //定义管道描述符
int sockfd;//定义 sock 描述符
int clientfd; //定义数据传输 sock 描述符
struct sockaddr_in host _addr;//本机IP地址和端口信息
struct sockaddr_in client_addr;//客户端IP地址和端口信息
socklen_t length = sizeof client_addr;
//创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);//TCP/IP协议,数据流套接字
if(sockfd == -1)//判断socket函数的返回值
{
printf("创建socket失败!\n");
return 0;
}
//绑定套接字
bzero(&host_addr, sizeof host_addr);
host_addr.sin_family = AF_INET;//TCP/IP协议
host_addr.sin_port = htons(SERV_PORT);//设定端口号
host_addr.sin_addr.s_addr = INADDR_ANY;//本地IP地址
ret = bind(sockfd, (struct sockaddr *)&host_addr, sizeof host_addr); //绑定套接字
if(ret == -1) //判断bind函数的返回值
{
printf("调用bind失败!\n");
return 1;
}
//监听网络端口
ret = listen(sockfd, BACKLOG);
if(ret==-1)//判断listen函数的返回值
{
printf("调用listen函数失败.\n");
return 1;
}
while(1)
{
clientfd = accept(sockfd,(struct sockaddr*)&client_addr, &length);//接收连接请求
if(clientfd = -1)
{
printf("调用accept接受连接失败.\n");
return 1;
}
pid = fork();//创建子进程
if(pid == 0)//在子进程中处理
{
while(1)
{
bzero(buf, sizeof buf);//首先清空缓冲区
tt = time(NULL);
ttm = localtime(&tt);//获取当前时间参数
strcpy(buf, asctime(ttm));//将时间信息 copy 进缓冲区
send(clientfd, buf, strlen(buf), 0);//发送数据
sleep(2);
}
close(clientfd);//调用close函数关闭连接
}
else if(pid > 0)
{
close(clientfd);//父进程关闭套接字,准备下一个客户端连接
}
}
return 0;
}
【例2】TCP 通信程序客户端
使用 argv[1] 作为连接的 IP 地址,将这个 IP 地址放入 serv_addr 所指定的地址结构体中,在创建套接字之后使用 connect 函数和服务器建立连接,使用 recv 接收服务器发送过来的时间信息并且打印输出。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERV_PORT 25555 //服务器接听端口号
#define BACKLOG 20 //请求队列中允许请求数
#define BUF_SIZE 256 //缓冲区大小
int main(int argc, char *argv[])
{
int ret;
char buf[BUF_SIZE];
int sockfd; //定义 sock 描述符
struct sockaddr_in serv_addr; //服务器 IP 地址和端口信息
if(argc != 2)
{
print("命令行输入有误.\n");//命令行带IP
return 1;
}
//创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);//TCP/IP协议,数据流套接字
if(sockfd = -1)
{
printf("调用socket函数失败.\n");
return 2;
}
//建立连接
bzero(&serv_addr, sizeof serv_addr);
serv_addr.sin_family = AF_INET;//TCP/IP协议
serv_addr.sin_port = htons(SERV_PORT);//设定端口号
serv_addr.sin_addr.s_addr = INADDR_ANY;//使用回环地址127.0.0.1
inet_aton(argv[1], (in_addr *)&serv_addr.sin_addr.s_addr);
ret = connect(sockfd, (struct sockaddr *)&serv_addr, sizeof serv_ addr); //绑定套接字
if(ret==-1)
{
printf("调用connect函数失败.\n");
return 3;
}
while(1)
{
bzero(buf, sizeof buf);
recv(sockfd, buf, sizeof(buf), 0);//接收数据
printf("接收到: %s", buf);
sleep(1);
}
close(sockfd); //关闭链接
return 0;
}
在两个不同的终端中分别执行以上两个程序生成的可执行文件,可以看到时间的输出。