目录
[一. TCP协议](#一. TCP协议)
[1.1 TCP协议及其特点](#1.1 TCP协议及其特点)
[1.2 TCP协议的机制](#1.2 TCP协议的机制)
[1.2.1 三次握手](#1.2.1 三次握手)
[1.2.2 四次挥手](#1.2.2 四次挥手)
[二. TCP编程](#二. TCP编程)
[2.1 TCP编程的流程步骤](#2.1 TCP编程的流程步骤)
[2.2 相关函数接口](#2.2 相关函数接口)
[2.2.1 listen](#2.2.1 listen)
[2.2.2 accept](#2.2.2 accept)
[2.2.3 connect](#2.2.3 connect)
[2.2.4 recv](#2.2.4 recv)
[2.2.5 send](#2.2.5 send)
[2.3 示例代码](#2.3 示例代码)
[2.3.1 示例代码一](#2.3.1 示例代码一)
[2.3.2 全双工聊天](#2.3.2 全双工聊天)
[2.3.3 文件发送](#2.3.3 文件发送)
[三. TCP报文与机制](#三. TCP报文与机制)
[3.1 TCP报文头部](#3.1 TCP报文头部)
[3.2 TCP机制](#3.2 TCP机制)
一. TCP协议
1.1 TCP协议及其特点
TCP(Transmission Control Protocol):传输控制协议(流式套接字) ,位于传输层
TCP协议的特点:
-
面向数据流
-
面向连接
-
安全可靠的传输协议
-
TCP机制复杂
1.2 TCP协议的机制
1.2.1 三次握手
**目的:**TCP建立连接的时候,需经过三次握手,为了确保安全可靠,确保通信双方都准备就绪,并
且三次握手必需由客户端发起
过程:
三次握手必须由客户端发起,客户端先发送一包请求建立连接的数据,带有一个SYN请求建
立连接标志位,代表请求建立连接
然后,服务端向客户端回复一个带有ACK响应报文标志位 与SYN请求建立连接标志位的数据
最后,客户端向服务端再回复一个ACK响应报文标志位

1.2.2 四次挥手
**四次挥手:**断开连接,确保通信双方在断开连接前都已经收发数据结束,可以由客户端发起,也可
以由服务端发起
过程:
设由客户端发起,客户端给服务端发送一包带有FIN请求断开连接标志的数据,这个标志位在
TCP的头部
服务端若没有数据发送,则向客户端回复带有ACK响应标志位的报文,若有数据发,则发送
带有PSH请求读取数据标志的数据
若无数据发送,则服务端给客户端发送带有FIN请求断开连接标志位的数据
最后客户端给服务端回复带有ACK的报文,断开连接成功。

二. TCP编程
2.1 TCP编程的流程步骤

详细步骤:

1、建立连接过程:
服务端调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后**,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。**
2、数据发送过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read 或者recv ,读socket 就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write 或者send 发送请求给服务器,服务器收到后从read 或者recv 返回,对客户端的请求进行处理,在此期间客户端调用read 或者recv 阻塞等待服务器的应答,服务器调用write 或者send 将处理结果发回给客户端,再次调用read 或者recv 阻塞等待下一条请求,客户端收到后从read 或者recv返回,发送下一条请求,如此循环下去。
3、断开连接过程:
如果客户端没有更多的请求了,就调用close 关闭连接,就像写端关闭的管道一样,服务器的read 或者recv 返回,这样服务器就知道客户端关闭了连接,也调用close 关闭连接。注意,任何一方调用close后,连接的两个传输方向都关闭,不能再发送数据了。
2.2 相关函数接口
2.2.1 listen

**作用:**设置同时与服务器建立连接的上限数(同时进行3次握手的客户数量)
**参数:**sockfd:监听套接字
backlog:允许同时监听的客户端的最大个数,上限128
eg:listen(fd, 4);操作系统会优化默认上限最大是128,故传4,10等其实都一样
**返回值:**成功0 失败-1且errno
2.2.2 accept

**作用:**阻塞等待客户端建立连接,接收完成三次握手的客户端
**参数:**sockfd:监听套接字
*address:保存接收到的客户端的地址信息
*address_len:接收到的客户端的地址大小
**返回值:**成功能够与服务器进行数据通信的socket对应的文件描述符 失败-1且errno
2.2.3 connect

**作用:**使用现有的socket与服务器建立连接,绑定服务器的ip地址与端口
**参数:**sockfd:要建立连接的套接字
*addr:服务端的地址信息
addlen:服务端地址的大小
**返回值:**成功0 失败-1且errno
**注意:**在socket与connect之间客户端并没有使用bind绑定自己的ip地址与端口号,如果不使用bind
函数绑定客户端地址结构,是允许的,会采用"隐式绑定",系统自动分配,
2.2.4 recv

**作用:**接收套接字上的数据
**参数:**sockfd:从此套接字上接收
*buf:存放数据的起始地址指针
len:希望读到的大小
flags:标志,默认传0
**返回值:**成功返回收到的字节数 失败-1且errno
**注意:**当对方关闭套接字时,recv返回0,注意与报式套接字的recvfrom做对比
2.2.5 send

**作用:**向套接字上发送数据
**参数:**sockfd:向此套接字上发送
*buf:存放数据的起始地址指针
len:要发送数据的大小
flags:标志,默认传0
**返回值:**成功返回实际发送的字节数 失败-1且errno
2.3 示例代码
2.3.1 示例代码一
客户端与服务端的通信
客户端
cpp
#define SER_PORT 50000
#define SER_IP "192.168.0.182"
struct sockaddr_in seraddr;
/初始化套接字
int init_tcp_cli()
{
int sockfd, ret;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0){
perror("socket()");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SER_PORT);
inet_pton(AF_INET, SER_IP, &seraddr.sin_addr);
/调用connect发送SYN请求连接标志
ret = connect(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
if(ret < 0){
close(sockfd);
perror("connect()");
return -1;
}
return sockfd;
}
/客户端
int main()
{
ssize_t size;
int sockfd;
char sbuff[64] = {0};/发送缓冲区
char dst[64] = {0};/存放服务端的ip地址
sockfd = init_tcp_cli();//初始化套接字
if(sockfd == -1){
exit(1);
}
inet_ntop(AF_INET, &seraddr.sin_addr, dst, sizeof(dst));/将网络ip转为主机ip
while(1){
fgets(sbuff, sizeof(sbuff), stdin);/从终端接收
sbuff[strlen(sbuff) - 1] = '\0';
size = send(sockfd, sbuff, strlen(sbuff), 0);
if(size < 0){
perror("send()");
break;
}
printf("size is %ld\n",size);
memset(sbuff, 0, sizeof(sbuff));
size = recv(sockfd, sbuff, sizeof(sbuff), 0);
if(size < 0){/接收出错
perror("recv()");
break;
}
else if(size == 0){/服务端断开连接
printf("服务器已断开连接\n");
break;
}
printf("[%s : %d]:%s\n",dst,
ntohs(seraddr.sin_port), sbuff);
}
close(sockfd);
return 0;
}
服务端
cpp
#define SER_PORT 50000
#define SER_IP "192.168.0.182"
/初始化监听套接字
int init_tcp_ser()
{
int lisfd, ret;
struct sockaddr_in seraddr;
/初始化服务端地址信息
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SER_PORT);
seraddr.sin_addr.s_addr = inet_addr(SER_IP);
lisfd = socket(AF_INET, SOCK_STREAM, 0);/创建监听套接字
if(lisfd < 0){
perror("socket()");
return -1;
}
/绑定服务器地址信息
ret = bind(lisfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
if(ret < 0){
close(lisfd);
perror("bind()");
return -1;
}
ret = listen(lisfd, 10);/设置同时与服务器建立连接的上限
if(ret < 0){
close(lisfd);
perror("listen()");
return -1;
}
return lisfd;
}
/服务端
int main()
{
int lisfd, connfd, ret;
struct sockaddr_in cliaddr;
ssize_t size;
char rbuff[64] = {0};/接收缓冲区
lisfd = init_tcp_ser();/初始化监听套接字
if(lisfd == -1){
exit(1);
}
socklen_t cli_len = sizeof(cliaddr);/初始化客户端的地址大小
/接收完成三次握手的客户端,返回一个通信套接字
connfd = accept(lisfd, (struct sockaddr*)&cliaddr, &cli_len);
if(connfd < 0){
perror("accept()");
close(lisfd);
exit(1);
}
printf("[%s : %d] gets online\n",inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port));
/开始接收
while(1){
memset(rbuff, 0, sizeof(rbuff));
size = recv(connfd, rbuff, sizeof(rbuff), 0);
if(size < 0){
perror("recv()");
break;
}
else if(size == 0){/表示对方已断开连接
printf("[%s : %d] offline\n",inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port));
break;
}
size = send(connfd, "收到了", 9, 0);/在linux中一个汉字占3字节
if(size < 0){
perror("send()");
break;
}
printf("[%s : %d] :%s\n",inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port), rbuff);
}
close(lisfd);
close(connfd);
return 0;
}
结果:

2.3.2 全双工聊天
客户端
cpp
#define SER_PORT 50000
#define SER_IP "192.168.0.182"
struct sockaddr_in seraddr;
//客户端
//发送线程
void *fsend(void *arg)
{
int sockfd = *((int*)arg);
char sbuff[64] = {0};
ssize_t size;
while(1){
fgets(sbuff, sizeof(sbuff), stdin);
sbuff[strlen(sbuff) - 1] = '\0';
size = send(sockfd, sbuff, strlen(sbuff), 0);
if(0 == strcmp(sbuff, ".quit")){
break;
}
if(size < 0){
perror("send()");
break;
}
}
pthread_exit(NULL);
}
//接收线程
void *frecv(void *arg)
{
int sockfd = *((int*)arg);
char rbuff[64] = {0};
ssize_t size;
while(1){
size = recv(sockfd, rbuff, sizeof(rbuff), 0);
if(size < 0){
perror("recv()");
break;
}
else if(size == 0){//表示对方断开连接
break;
}
if(0 == strcmp(rbuff, ".quit")){
break;
}
printf("[%s:%d]:%s\n",inet_ntoa(seraddr.sin_addr)
,ntohs(seraddr.sin_port), rbuff);
}
pthread_exit(NULL);
}
//初始化套接字
int init_tcp_cli()
{
int sockfd, ret;
sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if(sockfd < 0){
perror("sockfd()");
return -1;
}
//初始化服务器地址信息
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SER_PORT);
inet_pton(AF_INET, SER_IP, &seraddr.sin_addr);//将主机ip转为网络ip
//与服务器建立连接
ret = connect(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
if(ret < 0){
perror("connect()");
close(sockfd);
return -1;
}
return sockfd;
}
//客户端
int main()
{
int sockfd = init_tcp_cli();//初始化套接字
if(sockfd == -1){
exit(1);
}
//创建线程--将套接字文件描述符传参
pthread_t pid_send, pid_recv;
pthread_create(&pid_send, NULL, fsend, &sockfd);
pthread_create(&pid_recv, NULL, frecv, &sockfd);
//回收线程
pthread_join(pid_send, NULL);
pthread_join(pid_recv, NULL);
//关闭文件描述符
close(sockfd);
return 0;
}
服务端
cpp
#define SER_PORT 50000
#define SER_IP "192.168.0.182"
//由于要使用客户端地址多次,故定义全局
struct sockaddr_in cliaddr;
socklen_t cli_len = sizeof(cliaddr);
//服务端
//接收线程
void *frecv(void *arg)
{
int connfd = *((int*)arg);
char rbuff[64] = {0};
ssize_t size;
while(1){
memset(rbuff, 0, sizeof(rbuff));
size = recv(connfd, rbuff, sizeof(rbuff), 0);
if(size < 0){
perror("recv()");
break;
}
else if(size == 0){//表示对方断开连接
break;
}
if(0 == strcmp(rbuff, ".quit")){
break;
}
printf("[%s:%d]:%s\n",inet_ntoa(cliaddr.sin_addr)
, ntohs(cliaddr.sin_port), rbuff);
}
pthread_exit(NULL);
}
//发送线程
void *fsend(void *arg)
{
int connfd = *((int*)arg);
char sbuff[64] = {0};
ssize_t size;
while(1){
fgets(sbuff, sizeof(sbuff), stdin);
sbuff[strlen(sbuff) - 1] = '\0';
if(0 == strcmp(sbuff, ".quit")){
break;
}
size = send(connfd, sbuff, strlen(sbuff), 0);
if(size < 0){
perror("send()");
break;
}
}
pthread_exit(NULL);
}
//初始化监听套接字
int init_tcp_ser(int *plisfd)
{
int lisfd, connfd, ret;
struct sockaddr_in seraddr;
//初始化服务器地址信息
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SER_PORT);
inet_pton(AF_INET, SER_IP, &seraddr.sin_addr);
lisfd = socket(AF_INET, SOCK_STREAM, 0);//创建监听套接字
if(lisfd < 0){
perror("socket()");
return -1;
}
//绑定服务器地址信息
ret = bind(lisfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
if(ret < 0){
close(lisfd);
perror("bind()");
return -1;
}
ret = listen(lisfd, 10);//设置同时与服务器建立连接的上限
if(ret < 0){
close(lisfd);
perror("listen()");
return -1;
}
return lisfd;
}
//服务端
int main()
{
int connfd, lisfd;
lisfd = init_tcp_ser(&lisfd);
if(lisfd == -1){
exit(1);
}
//接收完成三次握手的客户端,返回一个通信套接字
connfd = accept(lisfd, (struct sockaddr*)&cliaddr, &cli_len);
if(connfd < 0){
close(lisfd);
perror("accept()");
exit(1);
}
printf("[%s : %d] gets online\n",inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port));
//创建线程
pthread_t pid_recv, pid_send;
pthread_create(&pid_send, NULL, fsend, &connfd);
pthread_create(&pid_recv, NULL, frecv, &connfd);
//回收线程
pthread_join(pid_recv, NULL);
pthread_join(pid_send, NULL);
close(lisfd);
close(connfd);
return 0;
}
结果:
可以看到一端在结束时不能发数据,但是可以接收数据

2.3.3 文件发送
客户端向服务端传输一个文件,并且要求双方的文件名一致
客户端
cpp
#define SER_PORT 50000
#define SER_IP "192.168.0.182"
//初始化套接字
int init_tcp_cli()
{
int sockfd, ret;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if(sockfd < 0){
perror("sockfd()");
return -1;
}
//初始化服务器地址信息
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SER_PORT);
inet_pton(AF_INET, SER_IP, &seraddr.sin_addr);
//与服务器建立连接
ret = connect(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
if(ret < 0){
close(sockfd);
perror("connect()");
return -1;
}
return sockfd;
}
//发送文件
int send_file(int sockfd, const char *arg)
{
ssize_t size;
int fd;
char sbuff[1024] = {0};
//发送文件名,在打开文件之前将文件名发送过去,不然会产生粘包问题
size = send(sockfd, arg, strlen(arg), 0);
if(size < 0){
close(sockfd);
perror("send()");
return -1;
}
//printf("name size is %ld\n",size);
printf("name send succeed!\n");
usleep(1);//休眠,防止产生粘包问题
//打开文件
fd = open(arg, O_RDONLY);
if(fd < 0){
close(sockfd);
perror("open()");
return -1;
}
while(1){
size = read(fd, sbuff, sizeof(sbuff));
if(size < 0){
perror("read()");
close(sockfd);
close(fd);
return -1;
}
else if(size == 0){//0表示读到了文件末尾
break;
}
size = send(sockfd, sbuff, size, 0);
if(size < 0){
perror("send()");
close(sockfd);
close(fd);
return -1;
}
}
close(fd);
return 0;
}
//客户端
int main(int argc, const char *argv[])
{
if(argc < 2){
fprintf(stderr,"Usage:....\n");
exit(1);
}
int sockfd, ret;
//初始化套接字
sockfd = init_tcp_cli();
if(sockfd == -1){
exit(1);
}
//发送文件
ret = send_file(sockfd, argv[1]);
if(ret == -1){
exit(1);
}
printf("file send succeed!\n");
close(sockfd);
return 0;
}
服务端
cpp
#define SER_PORT 50000
#define SER_IP "192.168.0.182"
//初始化套接字
int init_tcp_ser()
{
int lisfd, ret;
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SER_PORT);
inet_pton(AF_INET, SER_IP,&seraddr.sin_addr);
lisfd = socket(AF_INET, SOCK_STREAM, 0);//创建监听套接字
if(lisfd < 0){
perror("socket()");
return -1;
}
//绑定服务器地址信息
ret = bind(lisfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
if(ret < 0){
close(lisfd);
perror("bind()");
return -1;
}
ret = listen(lisfd, 10);//设置同时与服务器建立连接的上限
if(ret < 0){
close(lisfd);
perror("listen()");
return -1;
}
return lisfd;
}
//接收文件
int recv_file(int connfd)
{
char file_name[64];
char rbuff[512] = {0};
ssize_t size;
int fd;
//接收文件名
memset(file_name, 0, sizeof(file_name));
size = recv(connfd, file_name, sizeof(file_name), 0);
if(size < 0){
perror("recv()");
return -1;
}
printf("file_name is %s\n",file_name);
//打开文件
fd = open(file_name, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if(fd < 0){
perror("open()");
return -1;
}
while(1){
size = recv(connfd, rbuff, sizeof(rbuff), 0);
if(size < 0){
close(fd);
perror("recv()");
return -1;
}
else if(size == 0){//表示对方关闭了连接
break;
}
size = write(fd, rbuff, size);
if(size < 0){
perror("write()");
break;
}
}
close(fd);
return 0;
}
//服务端
int main(int argc, const char *argv[])
{
int lisfd, connfd, fd, ret;
struct sockaddr_in cliaddr;
socklen_t cli_len = sizeof(cliaddr);
//初始化监听套接字
lisfd = init_tcp_ser();
if(lisfd == -1){
exit(1);
}
//接收完成三次握手的客户端,返回一个通信套接字
connfd = accept(lisfd, (struct sockaddr*)&cliaddr, &cli_len);
if(connfd < 0){
close(lisfd);
perror("accept()");
exit(1);
}
//打印建立连接的客户端信息
printf("[%s : %d] gets online\n",inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port));
//接收文件
ret = recv_file(connfd);
if(ret == -1){
exit(1);
}
printf("file copy finish!\n");
close(lisfd);
close(connfd);
return 0;
}
结果:
如果客户端在发送完文件名后不休眠则会出现粘包问题:
可以看到文件名之后还有其他发过来的数据

粘包问题及解决方法:
粘包问题: 接收端应用层收到的数据多包数据粘连在一起。
原因:
-
由于收发双方速率不匹配,导致接收缓冲区缓存了多包数据,应用层读取时,一次性读出
-
发送端发送数据时将多包数据重新组包。发送端发送数据时,将多包数据重新组包。发送
端也有缓冲区,tcp底层机制为尽可能多发,故会粘包。
解决方法:
1、收发指定大小
比如发结构体类型的数据,但是又存在不同位数系统下结构体对齐的问题,在32为系统
long为4字节,64位系统位8字节,结构体在不同平台上字节数不一致问题,需考虑
2、每包数据增加分隔符,应用层根据分隔符解析
每包数据增加分割符,应用层根据分隔符解析,发字符串时用的多**\n \n\r \n\n**等等
3、带有帧头帧尾的自定义协议,应用层根据协议精准解析
帧头+有效数据长度+数据+校验和+帧尾
常用帧头和帧尾:AA BB 7E 5A与A5 EB90(航天常用)
常用校验方式:8位和校验(将有效数据累加,只保留低八位)
16和校验(将有效数据累加,保留高低八位,两字节)
CRC校验
cpp
AA 07 01 02 03 BB 05 06 07 sum BB
AA 0A 02 03 05 06 08 09 10 1A 2B 3C sum BB
AA 07 01 02 03 BB 05 06 07 sum BB AA 0A 02 03 05 06 08 09 10 1A 2B 3C sum BB
三. TCP报文与机制
3.1 TCP报文头部

TCP报头总共32字节:
源端口2字节
目标端口2字节
序列号4字节(本报文段携带数据第一个字节 的编号) 确认序列号4字节(实现累计确认 )
数据偏移:TCP头部的字节长度
窗口:2,控制流量
校验和:2字节
紧急指针:2字节
六个标志位:
|------------|------------------------------------|
| URG紧急位 | URG=1,紧急指针有效 |
| ACK确认位 | 除第一次 SYN 握手报文外,所有 TCP 报文 ACK 必须置 1 |
| PSH推送位 | 接收方收到后,立即把缓冲区全部数据上交应用程序,不要缓存等待更多数据 |
| RST复位位 | 强制断开连接 |
| SYN同步位 | 连接建立握手报文 |
| FIN结束位 | 本端不再发送数据,请求关闭单向数据流(四次挥手) |
3.2 TCP机制
3.2.1确保安全可靠的机制
**应答机制:**TCP对于发送的任意一包数据都有应答ACK,发送数据时,TCP头部的序列号表示这包
数据的第一个字节的编号(eg:1000字节:0~999,序号就是0),ACK响应时会将确认
号置位,填充成收到的数据最后一个字节序号+1,
**超时重传机制:**等待应答超时时,数据会被重新发送
**流量控制机制:**发送方会根据响应报文头部的窗口大小,动态调整发送速率。
**滑动窗口机制:**发送端会将已发送已收到应答,已发送未收到应答,以及即将发送的数据缓存
在滑动串口。
发送端的缓冲区,滑动窗口,多个指针将其分为了多块:已发送已收到应答 +已发
送未收到应答 +未发送但在对方处理范围之内 ,就是即将发送的数据
3.2.2提高效率机制
**延迟应答机制:**可以在发送的同时等待应答,并不是立马应答
**捎带应答机制:**TCP的应答有时可以和对方发送的正文数据一同发出