文章目录
目录
[1. 面向连接](#1. 面向连接)
[2. 可靠性](#2. 可靠性)
[3. 流量控制](#3. 流量控制)
[4. 拥塞控制](#4. 拥塞控制)
[5. 基于字节流](#5. 基于字节流)
[6. 全双工通信](#6. 全双工通信)
[7. 状态机](#7. 状态机)
[8. TCP头部结构](#8. TCP头部结构)
[9. TCP的应用场景](#9. TCP的应用场景)
[1. Socket 创建与配置](#1. Socket 创建与配置)
[2. 绑定与监听](#2. 绑定与监听)
[3. 连接与接受连接](#3. 连接与接受连接)
[4. 数据发送与接收](#4. 数据发送与接收)
[5. 关闭连接](#5. 关闭连接)
[6. 地址转换与解析](#6. 地址转换与解析)
[7. 错误处理](#7. 错误处理)
前言
-
TCP协议概述:简要介绍TCP协议的基本特性,包括连接建立、数据传输、流量控制、拥塞控制等。
-
Linux网络编程基础:介绍Linux下的Socket编程接口,以及如何使用这些接口进行TCP通信。
-
TCP服务器与客户端的实现:通过实际的代码示例,展示如何编写一个简单的TCP服务器和客户端程序。
-
TCP协议的性能优化:探讨如何通过调整TCP参数、使用非阻塞I/O、多线程/多进程等技术来提升TCP应用的性能。
-
常见问题与调试技巧:分享一些在实际开发中可能遇到的TCP相关问题,以及如何使用工具进行网络调试和故障排查。
一、TCP逻辑
1. 面向连接
TCP是一种面向连接的协议,这意味着在数据传输之前,通信双方需要先建立一个连接。连接的建立和关闭是通过三次握手和四次挥手来完成的。
三次握手(建立连接)
-
SYN:客户端向服务器发送一个SYN(同步)报文,表示请求建立连接。
-
SYN-ACK:服务器收到SYN后,回复一个SYN-ACK(同步-确认)报文,表示同意建立连接。
-
ACK:客户端收到SYN-ACK后,发送一个ACK(确认)报文,连接正式建立。
四次挥手(关闭连接)
-
FIN:主动关闭方(客户端或服务器)发送一个FIN(结束)报文,表示希望关闭连接。
-
ACK:被动关闭方收到FIN后,回复一个ACK报文,表示确认收到关闭请求。
-
FIN:被动关闭方完成数据发送后,发送一个FIN报文,表示自己也准备关闭连接。
-
ACK:主动关闭方收到FIN后,回复一个ACK报文,连接正式关闭。
2. 可靠性
TCP通过以下机制确保数据的可靠传输:
-
序列号与确认机制:每个TCP报文都包含一个序列号(Sequence Number),接收方通过发送确认号(Acknowledgment Number)来确认已收到的数据。如果发送方未收到确认,则会重传数据。
-
超时重传:如果发送方在一定时间内未收到确认,则会重新发送数据。
-
数据校验:TCP使用校验和(Checksum)来检测数据在传输过程中是否损坏。
3. 流量控制
TCP通过滑动窗口机制实现流量控制,防止发送方发送数据过快导致接收方缓冲区溢出。
-
接收窗口:接收方通过TCP头部中的窗口字段告知发送方自己当前可接收的数据量。
-
动态调整:接收方可以根据自身缓冲区的可用空间动态调整窗口大小。
4. 拥塞控制
TCP通过拥塞控制算法避免网络拥塞,常见的算法包括:
-
慢启动(Slow Start):初始时发送方以较小的窗口发送数据,随后指数增长。
-
拥塞避免(Congestion Avoidance):当窗口达到阈值后,发送方以线性方式增加窗口大小。
-
快速重传(Fast Retransmit):当发送方收到三个重复的ACK时,立即重传丢失的报文。
-
快速恢复(Fast Recovery):在快速重传后,发送方进入快速恢复阶段,避免窗口大幅减小。
5. 基于字节流
TCP是一种基于字节流的协议,这意味着:
-
无消息边界:TCP将数据视为连续的字节流,不保留发送方写入的数据边界。例如,发送方发送两次数据("Hello"和"World"),接收方可能一次性收到"HelloWorld"。
-
粘包与拆包:由于TCP的字节流特性,接收方需要自己处理数据的边界问题(如通过长度字段或特殊分隔符)。
6. 全双工通信
TCP支持全双工通信,即通信双方可以同时发送和接收数据。每个TCP连接由两个独立的流组成:
-
一个流用于从客户端到服务器的数据传输。
-
另一个流用于从服务器到客户端的数据传输。
7. 状态机
TCP连接的生命周期由一个状态机管理,常见的状态包括:
-
LISTEN:服务器等待客户端连接。
-
SYN_SENT:客户端已发送SYN,等待服务器响应。
-
SYN_RECEIVED:服务器已收到SYN并发送SYN-ACK,等待客户端确认。
-
ESTABLISHED:连接已建立,可以传输数据。
-
FIN_WAIT_1 / FIN_WAIT_2:主动关闭方等待对方的FIN或ACK。
-
CLOSE_WAIT:被动关闭方等待应用程序关闭连接。
-
TIME_WAIT:连接关闭后,等待可能出现的延迟报文。
8. TCP头部结构
TCP头部包含以下关键字段:
-
源端口和目的端口:标识通信的应用程序。
-
序列号和确认号:用于数据排序和确认。
-
标志位:如SYN、ACK、FIN等,用于控制连接状态。
-
窗口大小:用于流量控制。
-
校验和:用于数据完整性校验。
9. TCP的应用场景
TCP适用于需要可靠传输的场景,例如:
-
Web浏览(HTTP/HTTPS)
-
文件传输(FTP)
-
电子邮件(SMTP/POP3/IMAP)
-
远程登录(SSH/Telnet)
二、编写tcp代码函数
1. Socket 创建与配置
socket()
-
功能:创建一个新的套接字(socket),用于网络通信。
-
原型:
cppint socket(int domain, int type, int protocol);
-
参数:
-
domain
:协议族,如AF_INET
(IPv4)或AF_INET6
(IPv6)。 -
type
:套接字类型,如SOCK_STREAM
(TCP)或SOCK_DGRAM
(UDP)。 -
protocol
:通常为0
,表示默认协议。
-
-
返回值:成功返回套接字文件描述符,失败返回
-1
。
setsockopt()
-
功能:设置套接字选项,如重用地址、调整缓冲区大小等。
-
原型:
cppint setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
-
常用选项:
-
SO_REUSEADDR
:允许重用本地地址。 -
SO_RCVBUF
/SO_SNDBUF
:调整接收/发送缓冲区大小。
-
2. 绑定与监听
bind()
-
功能:将套接字绑定到本地地址和端口。
-
原型:
cppint bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
参数:
-
sockfd
:套接字文件描述符。 -
addr
:指向struct sockaddr
的指针,包含地址和端口信息。 -
addrlen
:地址结构体的长度。
-
listen()
-
功能:将套接字设置为监听模式,等待客户端连接。
-
原型:
cppint listen(int sockfd, int backlog);
-
参数:
-
sockfd
:套接字文件描述符。 -
backlog
:等待连接队列的最大长度。
-
3. 连接与接受连接
connect()
-
功能:客户端使用该函数连接到服务器。
-
原型:
cppint connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
参数:
-
sockfd
:套接字文件描述符。 -
addr
:指向服务器地址结构体的指针。 -
addrlen
:地址结构体的长度。
-
accept()
-
功能:服务器接受客户端的连接请求,返回一个新的套接字用于通信。
-
原型:
cppint accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
参数:
-
sockfd
:监听套接字文件描述符。 -
addr
:用于存储客户端地址信息。 -
addrlen
:地址结构体的长度。
-
4. 数据发送与接收
send()
-
功能:通过已连接的套接字发送数据。
-
原型:
cppssize_t send(int sockfd, const void *buf, size_t len, int flags);
-
参数:
-
sockfd
:套接字文件描述符。 -
buf
:指向要发送数据的缓冲区。 -
len
:数据长度。 -
flags
:标志位,通常为0
。
-
recv()
-
功能:通过已连接的套接字接收数据。
-
原型:
cppssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
参数:
-
sockfd
:套接字文件描述符。 -
buf
:指向接收数据的缓冲区。 -
len
:缓冲区长度。 -
flags
:标志位,通常为0
。
-
5. 关闭连接
close()
-
功能:关闭套接字,释放资源。
-
原型:
cppint close(int sockfd);
-
参数:
sockfd
:套接字文件描述符。
shutdown()
-
功能:优雅地关闭连接,可以选择关闭读、写或读写通道。
-
原型:
cppint shutdown(int sockfd, int how);
-
参数:
-
sockfd
:套接字文件描述符。 -
how
:关闭方式,如SHUT_RD
(关闭读)、SHUT_WR
(关闭写)、SHUT_RDWR
(关闭读写)。
-
6. 地址转换与解析
inet_pton()
-
功能:将点分十进制的IP地址转换为二进制格式。
-
原型:
cppint inet_pton(int af, const char *src, void *dst);
-
参数:
-
af
:地址族,如AF_INET
或AF_INET6
。 -
src
:点分十进制字符串。 -
dst
:存储二进制结果的缓冲区。
-
inet_ntop()
-
功能:将二进制格式的IP地址转换为点分十进制字符串。
-
原型:
cppconst char *inet_ntop(int af, const void *src, char *dst, socklen_t size); getaddrinfo()
-
功能:解析主机名和服务名,返回地址信息。
-
原型:
cppint getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
7. 错误处理
perror()
-
功能:打印与
errno
相关的错误信息。 -
原型:
cppvoid perror(const char *s);
strerror()
-
功能:将错误码转换为可读的字符串。
-
原型:
cppchar *strerror(int errnum);
三、基本客服端和服务端
1、服务端
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
char *response = "Hello from server";
// 1. 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket failed");
exit(EXIT_FAILURE);
}
// 2. 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 3. 监听连接
if (listen(server_fd, 3) < 0) {
perror("Listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("Accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 5. 读取客户端数据
int valread = read(new_socket, buffer, BUFFER_SIZE);
printf("Client says: %s\n", buffer);
// 6. 发送响应给客户端
send(new_socket, response, strlen(response), 0);
printf("Response sent to client\n");
// 7. 关闭连接
close(new_socket);
close(server_fd);
return 0;
}
2、客户端
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
char *message = "Hello from client";
// 1. 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 2. 将IP地址从字符串转换为二进制格式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
close(sock);
exit(EXIT_FAILURE);
}
// 3. 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection failed");
close(sock);
exit(EXIT_FAILURE);
}
// 4. 发送数据到服务器
send(sock, message, strlen(message), 0);
printf("Message sent to server\n");
// 5. 接收服务器的响应
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Server says: %s\n", buffer);
// 6. 关闭连接
close(sock);
return 0;
}
总结
TCP网络编程是构建高性能、高可靠性网络应用的基础。通过理解TCP协议的工作原理、掌握Linux Socket API的使用方法,并实践编写客户端和服务器程序,我们可以逐步掌握网络编程的核心技能。希望本篇博客能为你的学习之旅提供帮助,期待你在网络编程的世界中探索更多可能性!