目录
[1. TCP通信流程](#1. TCP通信流程)
[2. 服务器端的通信流程](#2. 服务器端的通信流程)
[2.1 创建用于监听的套接字](#2.1 创建用于监听的套接字)
[2.2 绑定本地IP地址和端口](#2.2 绑定本地IP地址和端口)
[2.3 设置监听](#2.3 设置监听)
[2.4 等待接受客户端的连接请求](#2.4 等待接受客户端的连接请求)
[2.5 与客户端进行通信](#2.5 与客户端进行通信)
[2.6 客户端连接服务器](#2.6 客户端连接服务器)
在本文中,我们将深入了解套接字(socket)及其在网络通信中的应用,特别是如何在服务器端创建一个基于TCP的简单通信框架。套接字是程序员进行网络通信的一组接口,主要分为客户端和服务器端。在这篇文章中,我们将重点关注服务器端的实现。
1. TCP通信流程
TCP(传输控制协议)是一个面向连接、安全可靠的流式传输协议,它位于传输层,确保数据的准确传输。

2. 服务器端的通信流程
下面是整个服务器端的通信流程:
2.1 创建用于监听的套接字
首先,我们需要创建一个套接字来监听客户端的连接请求。代码示例如下:
// 创建一个套接字,函数原型
int socket(int domain, int type, int protocol);
// 使用
int fd = socket(AF_INET, SOCK_STREAM, 0);
包含的头文件: #include <sys/socket.h>
参数说明:
domain
:地址族协议(如AF_INET
表示使用IPv4,AF_INET6
表示使用IPv6)type
:数据传输协议(SOCK_STREAM
表示TCP,SOCK_DGRAM
表示UDP)protocol
:一般设为0,表示使用默认协议
返回值: 成功返回可用于套接字通信的文件描述符,失败返回 -1。
2.2 绑定本地IP地址和端口
接下来,我们将文件描述符与本地IP地址和端口进行绑定。
// 函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 使用
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(10000);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
参数说明:
sockfd
:监听的文件描述符addr
:要绑定的IP和端口信息addrlen
:addr
指向的内存大小(sizeof(struct sockaddr)
)
返回值: 成功返回0,失败返回 -1。
2.3 设置监听
绑定后,接下来需要设置监听。
// 函数原型
int listen(int sockfd, int backlog);
// 使用
int ret = listen(fd, 128);
参数说明:
sockfd
:之前绑定的文件描述符backlog
:指定最大连接请求数
返回值: 成功返回0,失败返回 -1。
2.4 等待接受客户端的连接请求
现在我们需要等待并接受客户端的连接请求。
// 函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 使用
struct sockaddr caddr;
memset(&caddr, 0, sizeof(caddr));
socklen_t len = sizeof(caddr);
int cfd = accept(fd, (struct sockaddr*)&caddr, &len);
参数说明:
sockfd
:之前创建的文件描述符addr
:传出参数,存储客户端的地址信息addrlen
:传出参数,存储地址大小
返回值: 成功返回一个文件描述符,用于与客户端通信,失败返回 -1。注意,这个函数是阻塞的,直到有新的连接请求到来。
2.5 与客户端进行通信
接下来,可以通过read
和write
函数与客户端进行通信。
接收数据:
ssize_t read(int sockfd, void *buf, size_t size);
// 使用
char buf[1024] = {0};
int len = read(cfd, buf, sizeof(buf));
发送数据:
ssize_t write(int fd, const void *buf, size_t len);
// 使用
char msg[] = "Hello, Client!";
int size = write(cfd, msg, strlen(msg));
返回值说明:
- 接收数据(
read
):成功时返回接收到的字节数,连接断开返回0,失败返回-1。 - 发送数据(
write
):成功返回实际发送的字节数,失败返回-1。
2.6 客户端连接服务器
客户端时使用connect()
函数连接服务器,代码示例如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 使用
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(10000);
inet_pton(AF_INET, "1.1.1.1", &addr.sin_addr.s_addr); // 服务器IP地址
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
3.代码实现
server.cpp
#include <stdlib.h> // 提供exit函数
#include <stdio.h> // 提供printf和perror函数
#include <unistd.h> // 提供close函数
#include <arpa/inet.h> // 提供socket、bind、listen、accept等函数
#include <string.h> // 提供memset函数
int main()
{
// 1. 创建用于监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket"); // 输出错误信息
exit(1); // 退出程序
}
// 2. 绑定IP地址和端口
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr)); // 清空结构体
saddr.sin_family = AF_INET; // 使用IPv4
saddr.sin_port = htons(10000); // 监听端口,使用网络字节序
saddr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的接口
int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1) {
perror("bind"); // 输出错误信息
exit(1); // 退出程序
}
// 3. 设置监听
ret = listen(fd, 128); // 监听套接字,最大连接请求数为128
if (ret == -1) {
perror("listen"); // 输出错误信息
exit(1); // 退出程序
}
// 4. 等待并接受客户端的连接
struct sockaddr_in cliaddr; // 保存客户端的地址信息
socklen_t len = sizeof(cliaddr); // 地址结构体的大小
int cfd = accept(fd, (struct sockaddr*)&cliaddr, &len); // 阻塞等待连接
if (cfd == -1) {
perror("accept"); // 输出错误信息
exit(1); // 退出程序
}
// 打印新连接的客户端信息
char ip[64] = { 0 };
printf("new client fd:%d ip:%s, port:%d\n", cfd,
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)), // 获取客户端IP
ntohs(cliaddr.sin_port)); // 获取客户端端口
// 5. 与客户端进行通信
char buf[512];
while (1) {
memset(buf, 0, sizeof(buf)); // 清空缓冲区
int len = read(cfd, buf, sizeof(buf)); // 从客户端读取数据
if (len > 0) { // 成功接收到数据
printf("client says: %s\n", buf); // 打印客户端发送的消息
write(cfd, buf, len); // 将相同的数据回发给客户端
}
else if (len == 0) { // 客户端关闭了连接
printf("client is disconnect..\n");
break; // 退出循环
}
else { // 读取失败
perror("read"); // 输出错误信息
break; // 退出循环
}
}
close(fd); // 关闭监听套接字
close(cfd); // 关闭客户端连接套接字
return 0; // 程序结束
}
client.cpp
#include <stdlib.h> // 提供exit函数
#include <stdio.h> // 提供printf和perror函数
#include <unistd.h> // 提供close函数
#include <arpa/inet.h> // 提供socket、connect等函数
#include <string.h> // 提供memset和strlen函数
int main()
{
// 1. 创建套接字用于连接服务器
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket"); // 输出错误信息
exit(1); // 退出程序
}
// 2. 定义服务器地址和端口
struct sockaddr_in saddr; // 创建一个结构体用于存储服务器地址
memset(&saddr, 0, sizeof(saddr)); // 清空该结构体
saddr.sin_family = AF_INET; // 使用IPv4地址
saddr.sin_port = htons(10000); // 设置监听端口,使用网络字节序
inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr); // 设置服务器IP地址为localhost
// 3. 连接服务器
int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1) {
perror("connect"); // 输出错误信息
exit(1); // 退出程序
}
// 4. 与服务器进行通信
int n = 0; // 计数器
while (1)
{
// 发送数据
char buf[512] = { 0 }; // 初始化缓冲区
sprintf(buf, "hi, I am client...%d\n", n++); // 格式化字符串
write(fd, buf, strlen(buf)); // 发送数据到服务器
// 接收服务器反馈
memset(buf, 0, sizeof(buf)); // 清空缓冲区
int len = read(fd, buf, sizeof(buf)); // 从服务器读取数据
if (len > 0) // 成功接收到数据
{
printf("server say: %s\n", buf); // 打印服务器返回的数据
}
else if (len == 0) // 服务器关闭了连接
{
printf("server disconnect...\n");
break; // 退出循环
}
else // 读取失败
{
perror("read"); // 输出错误信息
break; // 退出循环
}
sleep(1); // 每隔1秒发送一条数据
}
close(fd); // 关闭套接字
return 0; // 程序结束
}
运行方式
-
编译代码: 在终端中使用
g++
编译器编译client.cpp
和server.cpp
。假设在同一目录下,输入以下命令:g++ server.cpp -o server g++ client.cpp -o client
-
运行服务器: 首先打开一个终端,运行服务器程序:
./server
-
运行客户端: 然后打开另一个终端,运行客户端程序:
./client
-
观察输出: 客户端将每隔一秒发送一条消息,服务器将在终端显示接收到的消息,并回发给客户端。您可以观察客户端和服务器的输出信息。
-
结束运行: 在客户端和服务器之间的通信完成后,可以在客户端终端按
Ctrl+C
结束客户端,服务器将自动检测到客户端的断开并输出相应消息。