接着上一篇,本篇主要是关于TCP协议,在此之前,先介绍一个网络抓包工具---wireshark。
一、网络抓包工具
wireshark是个网络抓包工具,可以抓取经过网卡的网络数据,用来分析网络问题和网络数据。
Linux下安装:
bash
sudo apt-get install wireshark
抓包流程:
-
启动wireshark:sudo wireshark
-
选择抓取的网卡:any
-
选择抓取数据包的过滤条件并触发,比如 udp.port == 50000
-
进行网络通信
二、TCP协议
TCP在传输层,叫传输控制协议,也叫流式套接字。
TCP特点
-
有连接
-
面向字节流
-
安全可靠的传输协议,靠三次握手、四次挥手、应答机制、超时重传机制这些保证
-
机制复杂,实时性和效率没有UDP高
常见用TCP的协议:HTTPS、MQTT、FTP。
三次握手和四次挥手
三次握手是TCP建立连接时用的,通过三次握手确保通信双方都已经准备就绪,由客户端发起。
四次挥手是TCP断开连接时用的,确保通信双方数据都已经收发结束。
TCP编程流程
客户端:socket() → connect() → send() → recv() → close()
服务端:socket() → bind() → listen() → accept() → recv() → send() → close()
和UDP比,服务端多了listen和accept两步,客户端多了connect,收发用send和recv而不是sendto和recvfrom。
相关函数
connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
请求建立连接。参数是套接字、服务端地址、地址长度。成功返回0,失败返回-1。
listen
int listen(int sockfd, int backlog);
监听客户端的三次握手。backlog是最多允许监听的客户端个数。成功返回0,失败返回-1。
accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
接收完成三次握手的客户端,并返回一个通讯套接字。参数里的addr用来保存接入的客户端地址信息。成功返回通讯套接字,失败返回-1。
注意这里返回的新套接字才是用来和客户端收发数据的,原来的sockfd继续负责监听。
recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
接收网络数据。参数是通讯套接字、存放数据的空间首地址、期望接收的字节数、flags(0默认方式)。
返回值:成功返回实际收到的字节数,失败返回-1,返回0表示对方断开连接。
TCP报文头部
TCP报文头部有几个标志位:
-
SYN:请求建立连接标志位
-
ACK:应答报文标志位
-
PSH:携带数据的报文标志位
-
FIN:请求断开连接标志位
-
URG:紧急数据标志位
-
RST:重置标志位
TCP机制
-
应答机制:TCP为发送的数据进行编号,发送数据时报文头部的序列号是这包数据第一个数据的编号;接收方需要给这包数据发ACK,ACK报文中确认号是收到的最后一个字节编号+1
-
超时重传机制:TCP每发送一包数据后都要等待应答,如果超时时间之内没有收到应答,就重新发送这包数据
-
滑动窗口机制:本质是个缓冲区,保存已发送并收到应答的数据、已发送未收到应答的数据、未发送但在对方处理范围内的数据
-
延迟应答机制:TCP可以发送多组数据,发送的同时等待应答
-
流量控制机制:TCP会根据发送端的数据处理和接收能力调整自己的发送速率,根据ACK中窗口值的大小进行动态调整流量
-
捎带应答机制:ACK可以和应用层发送的数据一起发出,表示对上包数据的响应
三、TCP客户端示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
perror("socket error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.119");
if (connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1) {
perror("connect error");
close(sockfd);
return -1;
}
char buff1024 = {0};
fgets(buff, sizeof(buff), stdin);
buffstrcspn(buff, "\\n") = '\0';
ssize_t size = send(sockfd, buff, strlen(buff), 0);
if (-1 == size) {
perror("send error");
close(sockfd);
return -1;
}
memset(buff, 0, sizeof(buff));
size = recv(sockfd, buff, sizeof(buff) - 1, 0);
if (-1 == size) {
perror("recv error");
} else if (0 == size) {
printf("服务器断开连接\n");
} else {
printf("收到: %s\n", buff);
}
close(sockfd);
return 0;
}
四、TCP服务端示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
perror("socket error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.119");
if (bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1) {
perror("bind error");
close(sockfd);
return -1;
}
if (listen(sockfd, 5) == -1) {
perror("listen error");
close(sockfd);
return -1;
}
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &addrlen);
if (-1 == connfd) {
perror("accept error");
close(sockfd);
return -1;
}
printf("客户端 %s:%d 接入\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
char buff1024 = {0};
while (1) {
memset(buff, 0, sizeof(buff));
ssize_t size = recv(connfd, buff, sizeof(buff) - 1, 0);
if (-1 == size) {
perror("recv error");
break;
}
if (0 == size) {
printf("客户端断开连接\n");
break;
}
printf("%s:%d : %s\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buff);
send(connfd, buff, size, 0);
}
close(connfd);
close(sockfd);
return 0;
}
TCP和UDP最大的区别就是TCP要先建立连接再通信,服务端需要listen监听、accept接收客户端,收发用send和recv,断开时走四次挥手。