前言
TCP和UDP是两种常见的网络通信形式,其中TCP 是 Transmission Control Protocol (传输控制协议)的简写,意为「对数据传输过程的控制」特征为:传输过程中数据不会消失、按序传输数据、传输的数据不存在数据边界,而UDP的特征为:强调快速传输而不是有序传输、传输的数据可能丢失损毁、传输的数据有边界以及限制每次传输数据的大小,因此TCP的可靠性要高于UDP。
下图为TCP通信的方式。
实现基于TCP的服务端
下图为TCP服务端函数的调用顺序
结合代码来理解
// 声明变量:sockfd用于服务器的套接字描述符,temp_result用于存储函数返回值,clientfd用于客户端的套接字描述符
int sockfd, temp_result, clientfd;
// 声明两个线程ID变量,分别用于读和写操作的线程
pthread_t pid_read, pid_write;
// 声明两个sockaddr_in结构体变量,分别用于存储服务器和客户端的地址信息
struct sockaddr_in server_addr, client_addr;
// 将server_addr和client_addr结构体中的所有字节置为0,以确保结构体中所有字段初始化为0
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 设置服务器地址信息
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置服务器的IP地址,INADDR_ANY表示绑定到所有可用的网络接口
server_addr.sin_port = htons(6666); // 设置服务器端口号,并将其从主机字节序转换为网络字节序,端口号为6666
// 创建套接字
//第一个参数指定用哪个协议族
//第二个参数指定用哪个数据传输方式
//第三个参数指定具体的协议信息
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个TCP套接字
handle_error("socket", sockfd); // 错误处理,如果失败则打印错误信息并退出
// 绑定套接字到服务器地址
temp_result = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 绑定套接字到指定的服务器地址和端口
handle_error("bind", temp_result); // 错误处理,如果失败则打印错误信息并退出
// 监听连接
temp_result = listen(sockfd, 128); // 监听连接请求,128表示等待队列的最大长度
handle_error("listen", temp_result); // 错误处理,如果失败则打印错误信息并退出
// 接受客户端连接
socklen_t cliaddr_len = sizeof(client_addr); // 定义变量存储客户端地址的长度
clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &cliaddr_len); // 接受客户端连接,并返回客户端文件描述符
handle_error("accept", clientfd); // 错误处理,如果失败则打印错误信息并退出
printf("与客户端%s %d建立连接 文件描述符是%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), clientfd); // 打印连接成功的信息
其中如果没有连接请求就调用accept函数的话,会一直挂起等待请求。
服务端完整代码如下:
#include <iostream> // 用于标准输入输出
#include <sys/socket.h> // 包含socket相关函数和数据结构的头文件
#include <sys/types.h> // 包含数据类型定义的头文件
#include <netinet/in.h> // 包含网络地址结构体定义的头文件
#include <stdlib.h> // 包含标准库函数的头文件
#include <string.h> // 包含字符串操作函数的头文件
#include <arpa/inet.h> // 包含网络操作函数的头文件
#include <pthread.h> // 包含线程相关函数和数据结构的头文件
#include <unistd.h> // 包含POSIX操作系统API的头文件
using namespace std; // 使用标准命名空间,简化代码
// 宏定义用于错误处理
#define handle_error(cmd, result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
}
// 线程函数:从客户端读取数据并输出到标准输出
void *read_from_client(void *arg)
{
char read_buf[1024]; // 定义一个缓冲区,用于存储从客户端读取的数据
int client_fd = *(int *)arg; // 从参数中获取客户端文件描述符
ssize_t count;
// 循环读取客户端数据并输出到标准输出
while ((count = recv(client_fd, read_buf, sizeof(read_buf), 0)) > 0)
{
fwrite(read_buf, 1, count, stdout); // 将读取的数据写入标准输出
}
// 检查读操作结果
if (count == 0)
{
printf("客户端请求关闭\n"); // 如果读取到的数据长度为0,表示客户端关闭了连接
}
else
{
perror("recv"); // 如果读取数据出错,打印错误信息
}
return NULL;
}
// 线程函数:读取标准输入并发送数据到客户端
void *write_to_client(void *arg)
{
char write_buf[1024]; // 定义一个缓冲区,用于存储将要发送的数据
int client_fd = *(int *)arg; // 从参数中获取客户端文件描述符
ssize_t count;
// 循环读取标准输入并发送到客户端
while (fgets(write_buf, sizeof(write_buf), stdin) != NULL)
{
count = send(client_fd, write_buf, strlen(write_buf), 0); // 发送数据到客户端
if (count < 0)
{
perror("send"); // 如果发送数据出错,打印错误信息
break;
}
}
printf("接收到关闭请求,关闭连接\n");
shutdown(client_fd, SHUT_WR); // 关闭写端,表示不再发送数据
return NULL;
}
int main()
{
// 声明变量:sockfd用于服务器的套接字描述符,temp_result用于存储函数返回值,clientfd用于客户端的套接字描述符
int sockfd, temp_result, clientfd;
// 声明两个线程ID变量,分别用于读和写操作的线程
pthread_t pid_read, pid_write;
// 声明两个sockaddr_in结构体变量,分别用于存储服务器和客户端的地址信息
struct sockaddr_in server_addr, client_addr;
// 将server_addr和client_addr结构体中的所有字节置为0,以确保结构体中所有字段初始化为0
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 设置服务器地址信息
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置服务器的IP地址,INADDR_ANY表示绑定到所有可用的网络接口
server_addr.sin_port = htons(6666); // 设置服务器端口号,并将其从主机字节序转换为网络字节序,端口号为6666
// 创建套接字
//第一个参数指定用哪个协议族
//第二个参数指定用哪个数据传输方式
//第三个参数指定具体的协议信息
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个TCP套接字
handle_error("socket", sockfd); // 错误处理,如果失败则打印错误信息并退出
// 绑定套接字到服务器地址
temp_result = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 绑定套接字到指定的服务器地址和端口
handle_error("bind", temp_result); // 错误处理,如果失败则打印错误信息并退出
// 监听连接
temp_result = listen(sockfd, 128); // 监听连接请求,128表示等待队列的最大长度
handle_error("listen", temp_result); // 错误处理,如果失败则打印错误信息并退出
// 接受客户端连接
socklen_t cliaddr_len = sizeof(client_addr); // 定义变量存储客户端地址的长度
clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &cliaddr_len); // 接受客户端连接,并返回客户端文件描述符
handle_error("accept", clientfd); // 错误处理,如果失败则打印错误信息并退出
printf("与客户端%s %d建立连接 文件描述符是%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), clientfd); // 打印连接成功的信息
// 创建读写线程
pthread_create(&pid_read, NULL, read_from_client, (void *)&clientfd); // 创建读线程
pthread_create(&pid_write, NULL, write_to_client, (void *)&clientfd); // 创建写线程
// 等待线程结束
pthread_join(pid_read, NULL); // 等待读线程结束
pthread_join(pid_write, NULL); // 等待写线程结束
printf("释放资源\n");
close(clientfd); // 关闭客户端套接字,释放资源
close(sockfd); // 关闭服务器套接字,释放资源
return 0;
}
实现基于TCP的客户端
客户端则比服务端逻辑简单一些了,客户端没有listen和accept函数了。
这是客户端的完整代码
#include <iostream> // 用于标准输入输出
#include <sys/socket.h> // 包含socket相关函数和数据结构的头文件
#include <sys/types.h> // 包含数据类型定义的头文件
#include <netinet/in.h> // 包含网络地址结构体定义的头文件
#include <stdlib.h> // 包含标准库函数的头文件
#include <string.h> // 包含字符串操作函数的头文件
#include <arpa/inet.h> // 包含网络操作函数的头文件
#include <pthread.h> // 包含线程相关函数和数据结构的头文件
#include <unistd.h> // 包含POSIX操作系统API的头文件
using namespace std; // 使用标准命名空间,简化代码
// 宏定义用于错误处理
#define handle_error(cmd, result) \
if (result < 0) { \
perror(cmd); \
return -1; \
}
// 线程函数:从服务器读取数据并输出到标准输出
void *read_from_server(void *arg) {
char read_buf[1024]; // 定义一个缓冲区,用于存储从服务器读取的数据
int server_fd = *(int *)arg; // 从参数中获取服务器文件描述符
ssize_t count;
// 循环读取服务器数据并输出到标准输出
while ((count = recv(server_fd, read_buf, sizeof(read_buf), 0)) > 0) {
fwrite(read_buf, 1, count, stdout); // 将读取的数据写入标准输出
}
// 检查读操作结果
if (count == 0) {
printf("服务端请求关闭\n"); // 如果读取到的数据长度为0,表示服务器关闭了连接
} else {
perror("recv"); // 如果读取数据出错,打印错误信息
}
return NULL;
}
// 线程函数:读取标准输入并发送数据到服务器
void *write_to_server(void *arg) {
char write_buf[1024]; // 定义一个缓冲区,用于存储将要发送的数据
int server_fd = *(int *)arg; // 从参数中获取服务器文件描述符
ssize_t count;
// 循环读取标准输入并发送到服务器
while (fgets(write_buf, sizeof(write_buf), stdin) != NULL) {
count = send(server_fd, write_buf, strlen(write_buf), 0); // 发送数据到服务器
if (count < 0) {
perror("send"); // 如果发送数据出错,打印错误信息
break;
}
}
printf("接收到关闭请求,关闭连接\n");
shutdown(server_fd, SHUT_WR); // 关闭写端,表示不再发送数据
return NULL;
}
int main() {
int sockfd, temp_result;
pthread_t pid_read, pid_write; // 线程ID,用于创建读写线程
struct sockaddr_in server_addr, client_addr; // 用于存储服务器和客户端地址信息
memset(&server_addr, 0, sizeof(server_addr)); // 初始化服务器地址结构体,清零
memset(&client_addr, 0, sizeof(client_addr)); // 初始化客户端地址结构体,清零
// 设置客户端地址信息
client_addr.sin_family = AF_INET; // 设置地址族为IPv4
inet_pton(AF_INET, "192.168.198.128", &client_addr.sin_addr); // 将IP地址从字符串转换为二进制格式
client_addr.sin_port = htons(6667); // 设置客户端端口号,并将其从主机字节序转换为网络字节序
// 设置服务器地址信息
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); // 将服务器IP地址从字符串转换为二进制格式
server_addr.sin_port = htons(6666); // 设置服务器端口号,并将其从主机字节序转换为网络字节序
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个TCP套接字
handle_error("socket", sockfd); // 错误处理,如果失败则打印错误信息并退出
// 绑定套接字到客户端地址
temp_result = bind(sockfd, (struct sockaddr *)&client_addr, sizeof(client_addr)); // 绑定套接字到指定的客户端地址和端口
handle_error("bind", temp_result); // 错误处理,如果失败则打印错误信息并退出
// 连接到服务器
temp_result = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 尝试连接到服务器
handle_error("connect", temp_result); // 错误处理,如果失败则打印错误信息并退出
printf("连接上服务端%s %d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port)); // 打印连接成功的信息
// 创建读写线程
pthread_create(&pid_read, NULL, read_from_server, (void *)&sockfd); // 创建读线程
pthread_create(&pid_write, NULL, write_to_server, (void *)&sockfd); // 创建写线程
// 等待线程结束
pthread_join(pid_read, NULL); // 等待读线程结束
pthread_join(pid_write, NULL); // 等待写线程结束
printf("释放资源\n");
close(sockfd); // 关闭套接字,释放资源
return 0;
}
注意,ip地址要改成自己的ip地址。
然后编译运行结果如下
实现了TCP通信
总结
TCP通信的代码量不多,如果需要实现两台Linux系统进行通信,则需要看http://t.csdnimg.cn/LFN6O这一篇进行ip配置等一系列工作,然后在代码中将客户端和服务端的ip地址改成两台电脑的ip就可以实现了