TCP协议


由于TCP协议是面向连接的,所以只能支持一对一服务,不提供广播服务和组播服务。

TCP协议传输数据时会为每个字节分配一个序列号,通过这个序列号就可以判断数据是否重复到达以及数据是否丢失,还可以通过序列号对数据进行排序。
另外,接收端每次接收到数据之后必须发送确认应答(ACK),如果在超时时间内没有发送应答信号(ACK),则TCP协议会认为数据没有送达,则会重新发送数据。
TCP协议判断数据是否损坏的方式是给每个数据段都添加校验和,接收端收到数据段进行校验,如果校验失败则丢弃已经损坏的数据段,也并不会进行确认应答,所以TCP协议会再次传输数据段。
TCP的连接机制

TCP协议中采用了一种基于时钟的序列号握手机制实现双方的有效连接,避免出现错误的连接初始化。
可以看到TCP协议头中存在6bit控制位,每个控制位的含义各不相同,其中较为常用的控制位是ACK(确认应答)、SYN(建立连接)、FIN(断开连接)。
ACK:该位是确认应答位,如果设置为1则表示支持确认应答,TCP规定该位必须设置为1。
SYN:该位是希望建立连接,另外可以在字段中会对序列号进行初始化,初始化为ISN的值。
FIN:该位设置为1表示不再发送数据,就是希望结束连接,通信结束时双方主机交换即可。
Linux系统提供了和TCP通信相关的函数接口,这些接口和UDP协议的接口有部分不同,可以阅读man手册的第7章和TCP通信相关的内容,终端输入 man 7 tcp

(1)发送端(客户端)
一般把发送端也称为主动连接端,实际情况中作为客户端,客户端首先要调用socket()函数创建TCP套接字,然后调用connect()函数来连接对方的TCP套接字,如果连接成功,则调用send()/write()函数发送数据即可。
1)socket()函数

可以看到,SOCK_STREAM类型的套接字提供有序的、可靠的基于字节流的全双工连接,其实就是指TCP协议,所以需要先调用socket()函数创建TCP套接字,并且类型选择SOCK_STREAM。
2)connect()函数
SOCK_STREAM类型的流式套接字在收发数据之前必须处于连接状态,因为TCP协议是面向连接的,只有连接成功,才可以通过send()函数和recv()函数进行数据收发。
所以发起连接的一方需要调用connect()函数进行连接,但是注意:调用connect()函数连接可能会失败,因为只有对方接收连接请求,才可以建立连接。connect()函数使用规则如下:

1.函数参数:
第一个参数:sockfd指的是套接字的文件描述符,其实是socket()函数的返回值,是个整数。
第二个参数:addr指的是目标主机的地址,结构体指针记录了目标主机的IP地址和端口号。
第三个参数:addrlen指的是目标主机的地址结构的大小,以字节为单位,sizeof()计算即可。
2.返回结果
connect()函数调用成功则返回0,connect()函数调用失败则返回-1,一般情况下基于连接的套接字只能成功连接一次。
3.注意事项
如果调用connect()函数连接目标主机失败,则调用方的套接字状态是未指定的,所以应该关闭套接字,并重新创建一个新的套接字,然后再次连接即可。
3)send()函数
一旦被连接端接受连接,就表示发起连接端调用connect()函数连接成功,则双方可以进行数据收发,此时可以调用send()函数发送数据,函数使用规则如下所示:

1.函数参数
第一个参数:sockfd是发送套接字的文件描述符,其实是socket()函数的返回值,是个整数。
第二个参数:buf指的是待发送数据的缓冲区,也就是要发送的数据的存储位置,是个指针。
第三个参数:len指的是待发送数据的大小,是以字节为单位,也就是要发送的数据的长度。
第四个参数:flags指的是发送标志,当flags等于0时,send()函数的效果与write()函数相同。
2.返回结果
函数调用成功,则返回实际发送的字节个数,函数调用失败,则返回-1,并且会返回错误码。
3.注意事项
send()函数只能用于套接字处于连接的状态,因为已经连接的套接字记录了接收方的地址信息。
另外,如果要发送的数据的不适合套接字的发送缓冲区,则send()函数通常会阻塞,除非套接字设置为非阻塞模式(如果套接字处于非阻塞模式,则send()函数会调用失败)。
(2)接收端(服务器)
一般把接收端称为被动连接端,实际情况中经常作为服务器,服务器首先需要创建TCP套接字,然后调用bind()函数绑定本地地址和端口号到套接字,然后调用listen()函数把TCP套接字设置为监听模式,如果处于监听模式下有客户端发送连接请求,如果打算接受连接请求,则调用accept()进行连接,连接成功后双方就可以调用recv()或者send()函数进行通信。
1)listen()函数
TCP协议是面向连接的,所以双方通信之前必须建立连接(也就是客户端发起连接请求,并且服务器接受请求才算是连接成功),但是对于服务器而言并不清楚何时会收到客户端的连接请求,以及服务器也不清楚有多少个客户端发起了请求,所以就需要把服务器的TCP套接字设置为监听模式,Linux系统提供了名称叫做listen()的函数接口实现该功能,规则如下:

1.函数参数
第一个参数:sockfd指的是要被设置为监听状态的套接字,其实就是socket()函数的返回值。
第二个参数:backlog指的是等待接受连接的客户端的最大长度,这个值最大是128,如下:

2.返回结果
listen()函数调用成功则返回0,listen()调用失败则返回-1,注意:listen()函数是不会阻塞的!!!
2)accept()函数
当服务器通过listen()函数把套接字设置为监听状态并且设置好最大等待连接的客户端数量之后,服务器就可以调用accept()函数接受连接请求,函数使用规则如下所示:

1.函数参数
第一个参数:sockfd指的是要被设置为监听状态的套接字,其实就是socket()函数的返回值。
第二个参数:addr指的是记录对方主机地址的结构指针,注意是为了记录对方的地址信息。
第三个参数:addrlen指的是对方主机的地址信息长度,以字节为单位,注意必须要初始化。
2.返回结果
accept()函数调用成功,会返回一个新的套接字文件描述符,accept()函数调用失败则返回-1。
3.注意事项
服务器调用accept()函数会从等待连接的队列中提取第一个客户端进行连接,也就是和第一个发起连接请求的客户端连接,如果此时等待队列中没有等待连接的客户端,则accept()函数会阻塞,直到等待队列中存在客户端请求。
注意:服务器如果和客户端连接成功,accept()函数会返回一个新的套接字描述符,这个新的套接字描述符并不处于监听状态,也就是说服务器和连接成功的客户端需要通过这个新的文件描述符进行数据收发。
3)recv()函数
如果服务器和客户端成功建立连接,则双发可以进行数据收发,此时接收数据需要调用recv()函数实现,使用规则如下:

1.函数参数
第一个参数:sockfd指的是进行读写的套接字描述符,注意:应该是accept()函数的返回值!
第二个参数:buf指的是接收到的数据要存储的缓冲区,也就是收到的数据要存在哪个位置!
第三个参数:len指的是要接收的数据的大小,以字节为单位,用户可以自行设置数据大小。
第四个参数:flags指的是接收标志,如果没有特殊需求则可以设置为0,此时和read()类似。
2.返回结果
函数调用成功,则返回接收到的字节个数,函数调用失败则返回-1,如果对端已经关闭连接,则返回值为0。另外,如果没有收到数据,则recv()函数会默认阻塞,直到有数据到达。
服务器代码
点击查看代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
//TCP服务器代码 ./xxx port
int main(int argc, char const *argv[])
{
//1.创建TCP套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket == -1)
{
fprintf(stderr, "tcp socket error,errno:%d,%s\n",errno,strerror(errno));
exit(1);
}
//2.绑定自身的IP地址和端口
struct sockaddr_in host_addr;
host_addr.sin_family = AF_INET; //协议族,是固定的
host_addr.sin_port = htons(atoi(argv[1])); //目标端口,必须转换为网络字节序
host_addr.sin_addr.s_addr = htonl(INADDR_ANY); //目标地址 INADDR_ANY 这个宏是一个整数,所以需要使用htonl转换为网络字节序
bind(tcp_socket,(struct sockaddr *)&host_addr, sizeof(host_addr));
//3.设置监听 队列最大容量是5
listen(tcp_socket,5);
//4.等待接受客户端的连接请求
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int connect_fd = accept(tcp_socket,(struct sockaddr *)&client,&client_len); //会阻塞
char buf[128] = {0};
//5.说明双方建立连接,此时可以接收数据
while(1)
{
read(connect_fd,buf,sizeof(buf));
printf("recv from [%s],data is = %s\n", inet_ntoa(client.sin_addr) ,buf);
bzero(buf,sizeof(buf));
}
return 0;
}
客户端代码
点击查看代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
//TCP客户端代码 ./xxx port IP
int main(int argc, char const *argv[])
{
//1.创建TCP套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket == -1)
{
fprintf(stderr, "tcp socket error,errno:%d,%s\n",errno,strerror(errno));
exit(1);
}
//4.发起连接请求,等待接受服务器接受连接
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET; //协议族,是固定的
dest_addr.sin_port = htons(atoi(argv[1])); //服务器端口,必须转换为网络字节序
dest_addr.sin_addr.s_addr = inet_addr(argv[2]); //服务器地址 "192.168.64.xxx"
int ret = connect(tcp_socket,(struct sockaddr *)&dest_addr,sizeof(dest_addr));
if (ret < 0)
{
fprintf(stderr, "connect error,errno:%d,%s\n",errno,strerror(errno));
exit(1);
}
char buf[128] = {0};
//5.说明双方建立连接,此时可以接收数据
while(1)
{
printf("请输入:");
scanf("%s",buf);
write(tcp_socket,buf,sizeof(buf));
bzero(buf,128);
}
return 0;
}
浙公网安备 33010602011771号