1. TCP协议简介
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它在网络编程中广泛应用于需要可靠数据传输的场景,如文件传输、网页访问、邮件传输等。TCP通过三次握手建立连接,通过四次挥手释放连接,并提供了确认机制、流量控制、拥塞控制等功能,保证数据有序、无差错、不重复地传输。
在Linux系统中,TCP网络编程主要通过套接字(Socket)接口实现。本文将详细介绍TCP通信的基本流程、关键函数接口,并通过一个文件传输的示例代码帮助读者理解实际应用。
2. TCP通信流程
TCP通信分为服务器端和客户端两部分。服务器端通常被动等待连接,客户端主动发起连接。基本流程如下:
服务器端(收端)流程:
创建套接字(socket)
绑定地址和端口(bind)
监听连接请求(listen)
接受客户端连接(accept)
接收数据(recv)或发送数据(send)
关闭连接(close)
客户端(发端)流程:
创建套接字(socket)
连接服务器(connect)
发送数据(send)或接收数据(recv)
关闭连接(close)
下面用流程图示意:
text
服务器端:
socket() → bind() → listen() → accept() → recv()/send() → close()
↑
客户端:
socket() → connect() → send()/recv() → close()
3. 主要函数接口详解
以下函数均定义在 <sys/socket.h> 中,使用时需包含相关头文件。
3.1 socket() — 创建套接字
c
int socket(int domain, int type, int protocol);
-
功能:创建一个套接字,返回文件描述符。
-
参数:
-
domain:协议族,IPv4使用 AF_INET。
-
type:套接字类型,TCP使用 SOCK_STREAM(流式套接字)。
-
protocol:协议,通常填0,表示自动选择对应协议(TCP对应IPPROTO_TCP)。
-
-
返回值:成功返回新套接字的文件描述符,失败返回-1。
3.2 bind() — 绑定地址
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
功能:将套接字与本地IP地址和端口号绑定。
-
参数:
-
sockfd:socket返回的文件描述符。
-
addr:指向 struct sockaddr_in 结构体的指针,包含IP地址和端口号(注意类型转换)。
-
addrlen:地址结构体的长度。
-
-
返回值:成功返回0,失败返回-1。
-
注意:只能绑定自己的IP地址,端口号不能重复绑定。
3.3 listen() — 监听连接
c
int listen(int sockfd, int backlog);
-
功能:将套接字设置为监听状态,等待客户端连接。
-
参数:
-
sockfd:已绑定的套接字。
-
backlog:未处理的三次握手请求的最大排队个数(即已完成三次握手但尚未被accept的连接队列长度)。
-
-
返回值:成功返回0,失败返回-1。
3.4 accept() — 接受连接
c
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
功能:从监听队列中取出第一个连接请求,并返回一个新的套接字用于与该客户端通信。
-
参数:
-
sockfd:监听套接字。
-
addr:用于存放客户端地址信息的缓冲区指针,可为NULL。
-
addrlen:传入时指定addr缓冲区大小,返回时实际地址长度。
-
-
返回值:成功返回新的通信套接字文件描述符,失败返回-1。
3.5 connect() — 发起连接
c
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
功能:客户端向服务器发起连接请求(三次握手)。
-
参数:
-
sockfd:客户端套接字。
-
addr:服务器地址信息。
-
addrlen:地址长度。
-
-
返回值:成功返回0,失败返回-1。
3.6 send() — 发送数据
c
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
-
功能:通过已连接的套接字发送数据。
-
参数:
-
sockfd:通信套接字(服务器端accept返回的套接字或客户端socket)。
-
buf:待发送数据的缓冲区。
-
len:发送的数据长度。
-
flags:标志位,通常设为0。
-
-
返回值:成功返回实际发送的字节数,失败返回-1。
3.7 recv() — 接收数据
c
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
功能:从已连接的套接字接收数据。
-
参数:
-
sockfd:通信套接字。
-
buf:接收缓冲区。
-
len:缓冲区大小。
-
flags:标志位,通常设为0。
-
-
返回值:成功返回实际接收的字节数,若对方关闭连接则返回0,失败返回-1。
3.8 close() — 关闭套接字
c
int close(int fd);
-
功能:关闭套接字,释放资源。
4. 代码实例:使用TCP实现文件传输
下面实现一个简单的文件传输程序:客户端将本地文件发送给服务器,服务器接收并保存为另一个文件。
服务器端代码(tcp_server.c)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <save_filename>\\n", argv[0]);
exit(1);
}
int listen_fd, conn_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
FILE *fp;
// 1. 创建套接字
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket");
exit(1);
}
// 2. 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
server_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(listen_fd);
exit(1);
}
// 3. 监听
if (listen(listen_fd, 5) == -1) {
perror("listen");
close(listen_fd);
exit(1);
}
printf("Server listening on port %d…\\n", PORT);
// 4. 接受连接
conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
if (conn_fd == -1) {
perror("accept");
close(listen_fd);
exit(1);
}
printf("Client connected: %s:%d\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 打开文件准备写入
fp = fopen(argv[1], "wb");
if (fp == NULL) {
perror("fopen");
close(conn_fd);
close(listen_fd);
exit(1);
}
// 5. 接收数据并写入文件
int bytes;
while ((bytes = recv(conn_fd, buffer, BUFFER_SIZE, 0)) > 0) {
fwrite(buffer, 1, bytes, fp);
}
if (bytes == 0) {
printf("File transfer completed.\\n");
} else {
perror("recv");
}
fclose(fp);
close(conn_fd);
close(listen_fd);
return 0;
}
客户端代码(tcp_client.c)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <server_ip> <filename>\\n", argv[0]);
exit(1);
}
int sock_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
FILE *fp;
// 1. 创建套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket");
exit(1);
}
// 2. 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) {
perror("inet_pton");
close(sock_fd);
exit(1);
}
// 3. 连接服务器
if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("connect");
close(sock_fd);
exit(1);
}
printf("Connected to server %s:%d\\n", argv[1], PORT);
// 打开待发送文件
fp = fopen(argv[2], "rb");
if (fp == NULL) {
perror("fopen");
close(sock_fd);
exit(1);
}
// 4. 发送文件内容
int bytes;
while ((bytes = fread(buffer, 1, BUFFER_SIZE, fp)) > 0) {
if (send(sock_fd, buffer, bytes, 0) == -1) {
perror("send");
break;
}
}
printf("File sent.\\n");
fclose(fp);
close(sock_fd);
return 0;
}
编译与运行:
bash
gcc tcp_server.c -o server
gcc tcp_client.c -o client
# 先运行服务器(指定保存的文件名)
./server received_file
# 再运行客户端(指定服务器IP和要发送的文件)
./client 192.168.1.100 myfile.txt
5. UDP与TCP的区别
PDF中总结了UDP和TCP的主要区别,这里整理成表格形式:
| 资源开销 | 小(UDP包头8字节) | 大(TCP包头至少20字节) |
| 传输机制 | 简单,无连接 | 复杂,面向连接(三次握手、四次挥手) |
| 可靠性 | 不可靠,可能丢包、乱序 | 可靠,通过确认、重传、排序保证 |
| 流量控制 | 无 | 有滑动窗口机制 |
| 拥塞控制 | 无 | 有拥塞控制算法 |
| 应用场景 | 实时性要求高、允许丢包(如音视频、DNS) | 要求可靠传输(如文件、网页、邮件) |
TCP的复杂性带来了可靠性,但也增加了开销和延迟。UDP则轻量高效,适合实时通信。
6. 总结
本文详细介绍了Linux下TCP网络编程的基本流程和核心函数接口,并通过一个文件传输示例展示了实际应用。TCP的面向连接特性保证了数据传输的可靠性,适用于对数据完整性要求高的场景。掌握TCP编程是学习网络通信的基础,读者可以在此基础上进一步探索多线程并发服务器、IO多路复用等高级主题。
练习:客户端与服务端的双向通信
client:



serve:






