实现简单的TCP服务器

ops/2025/1/23 17:11:40/

首先我们先选择一个端口号用于 TCP 或 UDP 网络通信。如果你运行一个服务或应用程序,监听端口就是通过该端口接收来自客户端的请求。

这里我们选择2048

先在ubuntu系统中输入netstat -anop | grep 2048,会显示以下信息
在这里插入图片描述
这代表此时2048端口号没有被占用

我们在ubuntu系统中用c编写以下程序

#include <stdio.h>      // 标准输入输出库,用于打印和输入
#include <sys/socket.h> // 套接字相关函数
#include <netinet/in.h> // 定义了 IP 协议族相关的常量和结构体
#include <string.h>     // 字符串处理函数
#include <unistd.h>     // 系统调用接口,包括 close 函数
#include <arpa/inet.h>  // 提供了 IP 地址转换函数等int main() {// 创建一个 TCP 套接字,返回套接字描述符int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket");return -1;}// 配置服务器端的地址结构struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(struct sockaddr_in)); // 初始化地址结构为零serveraddr.sin_family = AF_INET;               // 使用 IPv4 地址serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到任意 IP 地址serveraddr.sin_port = htons(2048);             // 绑定到端口 2048// 将服务器端地址和端口绑定到套接字上if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {perror("bind");return -1;}// 开始监听端口,最多允许 10 个连接排队if (listen(sockfd, 10) == -1) {perror("listen");return -1;}// 等待客户端连接struct sockaddr_in clientaddr;socklen_t len = sizeof(clientaddr);int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); // 接受客户端连接if (clientfd == -1) {perror("accept");return -1;}printf("Client connected\n");// 接收客户端数据char buffer[128] = {0};  // 缓冲区存放接收到的数据int count = recv(clientfd, buffer, 128, 0); // 接收数据if (count == -1) {perror("recv");close(clientfd);return -1;}// 发送接收到的数据给客户端send(clientfd, buffer, count, 0);// 打印相关信息printf("sockfd: %d, clientfd: %d, count: %d, buffer: %s\n", sockfd, clientfd, count, buffer);// 等待用户输入后关闭客户端连接getchar();close(clientfd);  // 关闭客户端套接字return 0; // 程序正常结束
}

代码解释:

  1. 创建套接字

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    

    使用 socket() 创建一个 TCP 套接字。AF_INET 表示使用 IPv4 地址族,SOCK_STREAM 表示创建一个流式套接字(TCP)。

  2. 初始化服务器地址结构

    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(struct sockaddr_in));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2048);
    

    创建并初始化一个 sockaddr_in 结构体,配置服务器的地址和端口:

    • sin_family 设置为 AF_INET 表示 IPv4。
    • sin_addr.s_addr 使用 htonl(INADDR_ANY) 绑定到本地所有可用的网络接口。
    • sin_port 设置为 2048,通过 htons() 进行主机字节序到网络字节序的转换。
  3. 绑定套接字到地址

    if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {perror("bind");return -1;
    }
    

    将套接字与指定的 IP 地址和端口绑定。bind() 用于将套接字与服务器地址结构绑定。

  4. 监听客户端连接

    if (listen(sockfd, 10) == -1) {perror("listen");return -1;
    }
    

    启动监听,参数 10 表示允许最多 10 个连接排队。

  5. 接受客户端连接

    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    

    accept() 阻塞等待客户端连接,接受连接请求并返回一个新的套接字 clientfd 用于与客户端通信。

  6. 接收和发送数据

    int count = recv(clientfd, buffer, 128, 0);
    send(clientfd, buffer, count, 0);
    
    • recv() 从客户端接收数据,存储到 buffer 中,最多接收 128 字节。
    • send() 将接收到的数据重新发送给客户端。
  7. 关闭连接

    close(clientfd);
    

    在交互结束后,关闭客户端连接。

编写完成后我们进行编译和运行
在这里插入图片描述
此时程序处于运行中

我们在系统再运行一次netstat -anop | grep 2048显示
在这里插入图片描述
可以看到此时2048端口处于监听状态

这时我们打开网络调试助手

  • 协议类型选TCP Client
  • 远程主机地址就是虚拟机里ubuntu系统的ip地址
  • 远程主机端口就是刚刚的2048

在这里插入图片描述
点击连接
在这里插入图片描述
c程序这里也显示连接到了
在这里插入图片描述
再试一次netstat -anop | grep 2048
在这里插入图片描述
可以看到显示有一个客户端连接到了

这时我们在网络调试助手发送消息
在这里插入图片描述
c程序也接受到了消息并返回了相应的信息
在这里插入图片描述

但这个c程序创建的服务器是单线程的,可以使用更多的客户端进行连接,但是不能发送消息,而解决方法就是创建一个多线程的服务器,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 2048      // 服务器监听的端口号
#define MAX_CLIENTS 10 // 最大客户端连接排队数// 客户端处理函数,每个客户端连接会启动一个线程来执行此函数
void* handle_client(void* arg) {int clientfd = *((int*)arg);  // 获取客户端的套接字描述符char buffer[128];             // 缓冲区,用于接收和发送数据int count;// 接收客户端发送的数据count = recv(clientfd, buffer, sizeof(buffer), 0);if (count == -1) {perror("recv");  // 如果接收出错,输出错误信息并关闭连接close(clientfd);pthread_exit(NULL);  // 结束线程}// 将接收到的数据发送回客户端(回显功能)send(clientfd, buffer, count, 0);printf("Received and sent back data: %s\n", buffer); // 打印接收到的数据// 关闭与客户端的连接close(clientfd);pthread_exit(NULL);  // 结束线程
}int main() {// 创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket");  // 如果创建套接字失败,输出错误信息并返回return -1;}// 配置服务器地址信息struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));  // 清零结构体serveraddr.sin_family = AF_INET;              // 使用 IPv4 地址族serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的网卡地址serveraddr.sin_port = htons(PORT);            // 设置服务器监听端口// 绑定套接字到指定的地址和端口if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {perror("bind");  // 如果绑定失败,输出错误信息并返回close(sockfd);return -1;}// 开始监听端口,最多允许 10 个连接排队if (listen(sockfd, MAX_CLIENTS) == -1) {perror("listen");  // 如果监听失败,输出错误信息并返回close(sockfd);return -1;}printf("Server listening on port %d...\n", PORT);  // 打印服务器监听信息// 变量定义:用于存储客户端地址信息和客户端套接字描述符struct sockaddr_in clientaddr;socklen_t len = sizeof(clientaddr);int clientfd;pthread_t thread_id;  // 线程标识符while (1) {// 接受客户端连接请求clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);if (clientfd == -1) {perror("accept");  // 如果接受连接失败,输出错误信息并跳过当前循环continue;}// 打印客户端的 IP 地址和端口号printf("New client connected from %s:%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));// 创建新的线程来处理该客户端的请求if (pthread_create(&thread_id, NULL, handle_client, (void*)&clientfd) != 0) {perror("pthread_create");  // 如果线程创建失败,输出错误信息并关闭客户端连接close(clientfd);continue;}// 将线程分离,线程结束时自动回收资源pthread_detach(thread_id);}// 关闭服务器套接字,退出程序close(sockfd);return 0;
}

1. 包含头文件

#include <stdio.h>      // 标准输入输出库,用于打印和输入
#include <stdlib.h>     // 用于内存分配、退出程序等
#include <string.h>     // 字符串操作函数
#include <unistd.h>     // 系统调用接口,包含 close 函数等
#include <pthread.h>    // 用于线程操作的库
#include <sys/socket.h> // 套接字相关函数
#include <netinet/in.h> // 定义了 IP 协议族相关的常量和结构体
#include <arpa/inet.h>  // 提供了 IP 地址转换函数等

这些是程序所需要的头文件。它们提供了处理套接字、网络通信、线程管理等功能所需的函数和常量。

  • stdio.h:标准输入输出库,提供 printf, perror, scanf 等函数。
  • stdlib.h:提供动态内存分配(如 malloc)和程序退出(如 exit)等功能。
  • string.h:提供字符串处理函数(如 memset, strlen)。
  • unistd.h:系统调用接口,提供诸如 close 等系统调用。
  • pthread.h:线程管理库,提供创建、管理、同步线程的功能。
  • sys/socket.hnetinet/in.h:提供创建和操作套接字的功能。
  • arpa/inet.h:提供 IP 地址相关的函数,如 inet_ntoa 用于将 in_addr 转换为可读的 IP 地址。

2. 定义常量

#define PORT 2048      // 服务器监听的端口号
#define MAX_CLIENTS 10 // 最大客户端连接排队数
  • PORT:定义了服务器监听的端口号,设置为 2048。
  • MAX_CLIENTS服务器在等待连接时,最多允许 10 个客户端请求排队。

3. handle_client 函数

void* handle_client(void* arg) {int clientfd = *((int*)arg);  // 获取客户端的套接字描述符char buffer[128];             // 缓冲区,用于接收和发送数据int count;// 接收客户端发送的数据count = recv(clientfd, buffer, sizeof(buffer), 0);if (count == -1) {perror("recv");  // 如果接收出错,输出错误信息并关闭连接close(clientfd);pthread_exit(NULL);  // 结束线程}// 将接收到的数据发送回客户端(回显功能)send(clientfd, buffer, count, 0);printf("Received and sent back data: %s\n", buffer); // 打印接收到的数据// 关闭与客户端的连接close(clientfd);pthread_exit(NULL);  // 结束线程
}
  • 参数说明

    • arg 是一个 void* 类型的指针,指向传递给线程的参数。在这个例子中,传递的是客户端套接字的描述符。
  • 接收客户端数据

    count = recv(clientfd, buffer, sizeof(buffer), 0);
    
    • recv() 函数用来从客户端读取数据。它将客户端发送的数据存储到 buffer 中,最多读取 sizeof(buffer) 字节的数据。
    • 如果读取出错,recv() 会返回 -1,程序会输出错误信息并关闭客户端连接。
  • 回显数据

    send(clientfd, buffer, count, 0);
    
    • send() 用于将从客户端接收到的数据发送回去。这里通过回显功能简单地将收到的数据重新发送给客户端。
  • 关闭连接并结束线程

    close(clientfd);
    pthread_exit(NULL);
    
    • 使用 close(clientfd) 关闭客户端连接。
    • pthread_exit(NULL) 结束线程的执行。

4. main 函数

int main() {// 创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket");  // 如果创建套接字失败,输出错误信息并返回return -1;}// 配置服务器地址信息struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));  // 清零结构体serveraddr.sin_family = AF_INET;              // 使用 IPv4 地址族serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的网卡地址serveraddr.sin_port = htons(PORT);            // 设置服务器监听端口// 绑定套接字到指定的地址和端口if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {perror("bind");  // 如果绑定失败,输出错误信息并返回close(sockfd);return -1;}// 开始监听端口,最多允许 10 个连接排队if (listen(sockfd, MAX_CLIENTS) == -1) {perror("listen");  // 如果监听失败,输出错误信息并返回close(sockfd);return -1;}printf("Server listening on port %d...\n", PORT);  // 打印服务器监听信息// 变量定义:用于存储客户端地址信息和客户端套接字描述符struct sockaddr_in clientaddr;socklen_t len = sizeof(clientaddr);int clientfd;pthread_t thread_id;  // 线程标识符while (1) {// 接受客户端连接请求clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);if (clientfd == -1) {perror("accept");  // 如果接受连接失败,输出错误信息并跳过当前循环continue;}// 打印客户端的 IP 地址和端口号printf("New client connected from %s:%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));// 创建新的线程来处理该客户端的请求if (pthread_create(&thread_id, NULL, handle_client, (void*)&clientfd) != 0) {perror("pthread_create");  // 如果线程创建失败,输出错误信息并关闭客户端连接close(clientfd);continue;}// 将线程分离,线程结束时自动回收资源pthread_detach(thread_id);}// 关闭服务器套接字,退出程序close(sockfd);return 0;
}
1. 创建服务器套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • 通过 socket() 创建一个 TCP 套接字。AF_INET 表示使用 IPv4 地址族,SOCK_STREAM 表示使用 TCP 协议(流式套接字)。

  • 如果套接字创建失败,返回 -1,程序通过 perror() 输出错误信息并退出。

2. 配置服务器地址
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));  // 清零结构体
serveraddr.sin_family = AF_INET;              // 使用 IPv4 地址族
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的网卡地址
serveraddr.sin_port = htons(PORT);            // 设置服务器监听端口
  • serveraddr 是一个 sockaddr_in 结构体,用于保存服务器的地址信息。
  • sin_family 设置为 AF_INET,表示 IPv4。
  • sin_addr.s_addr = htonl(INADDR_ANY),表示将服务器绑定到所有可用的网卡地址。
  • sin_port = htons(PORT),表示将服务器端口设置为预定的 PORT(2048)。
3. 绑定套接字到指定的地址和端口
bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
  • bind() 函数将服务器的套接字 sockfdserveraddr 中存储的地址和端口绑定。
4. 开始监听端口
listen(sockfd, MAX_CLIENTS);
  • listen() 函数使套接字进入监听模式,MAX_CLIENTS 定义了最多可以排队等待的客户端连接数。
5. 接受客户端连接
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
  • accept() 函数接受一个客户端连接请求,返回一个新的套接字 clientfd 用于与客户端进行通信。客户端的地址信息保存在 `

clientaddr` 中。

6. 创建线程处理客户端请求
pthread_create(&thread_id, NULL, handle_client, (void*)&clientfd);
  • 使用 pthread_create() 创建一个新线程来处理客户端请求。
  • handle_client 是线程运行的函数,参数传入客户端套接字 clientfd
7. 线程分离
pthread_detach(thread_id);
  • pthread_detach() 将线程设置为分离状态。线程结束后系统会自动回收资源,无需手动 join
8. 关闭服务器套接字
close(sockfd);
  • 程序退出前,关闭服务器的监听套接字 sockfd

总结:

  • 该代码实现了一个多线程的 TCP 服务器,能够同时处理多个客户端连接。
  • 每当有一个新的客户端连接,服务器创建一个新的线程来处理该连接,避免了串行处理多个客户端带来的延迟。

http://www.ppmy.cn/ops/152515.html

相关文章

Spring Boot 3.4.x 和 Micrometer 2.0 的结合 案例 以及使用方法

Spring Boot 3.4.x 和 Micrometer 2.0 的结合&#xff0c;主要是为了更好地进行应用性能监控。Micrometer 是一个应用性能监控工具&#xff0c;它可以与 Spring Boot 集成&#xff0c;提供一个统一的度量系统&#xff0c;并与各种监控系统&#xff08;如 Prometheus, Graphite,…

Linux探秘坊-------4.进度条小程序

1.缓冲区 #include <stdio.h> int main() {printf("hello bite!");sleep(2);return 0; }执行此代码后&#xff0c;会 先停顿两秒&#xff0c;再打印出hello bite&#xff0c;但是明明打印在sleep前面&#xff0c;为什么会后打印呢&#xff1f; 因为&#xff…

1.移动零

LeetCode 283. 移动零 1. 题目描述 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 注意&#xff1a;必须在 原地 对数组进行操作&#xff0c;不得额外分配新数组。 示例 示例 1: 输入: nums [0,1,0,3,1…

试题转excel;word转excel;大风车excel(1.1更新)

更新了大风车excel1.1版本 主要优化在算法层面&#xff1a; 1.0版本试题解析的成功率为95%&#xff0c;现在1.1版本已经优化到解析成功率为99% 一、问题描述 一名教师朋友&#xff0c;偶尔会需要整理一些高质量的题目到excel中 以往都是手动复制搬运&#xff0c;几百道题几…

HippoRAG:受海马体启发的长时记忆模型,提升大语言模型的知识整合能力

论文地址&#xff1a;https://arxiv.org/pdf/2405.14831 1. 背景与挑战 1.1 哺乳动物大脑与长时记忆 进化优势: 哺乳动物的大脑进化出强大的长时记忆系统&#xff0c;能够存储大量关于世界的知识&#xff0c;并不断整合新信息&#xff0c;同时避免灾难性遗忘。知识整合能力: …

代码随想录day15

110. 知道平衡二叉树的概念即可。 /** lc appleetcode.cn id110 langcpp** [110] 平衡二叉树*/// lc codestart /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nul…

AI赋能前端性能工程:突破技术边界,迈向智能化开发

前端性能工程对于用户体验至关重要。一个加载缓慢、反应迟钝的网站会直接导致用户流失&#xff0c;影响业务转化率。然而&#xff0c;在快速迭代的现代前端开发中&#xff0c;我们常常面临着效率瓶颈&#xff1a;代码冗余、资源加载缓慢、渲染性能低下等问题层出不穷。传统的手…

青少年CTF练习平台 PHP的XXE

访问靶场是个phpinfo()页面 题目提示是PHP的XXE&#xff0c;访问simplexml_load_string.php文件 get请求是空白&#xff0c;要使用post方法请求 尝试读取文件,读取/etc/passwd文件 <?xml version"1.0" encoding"utf-8" ?> <!DOCTYPE xxe [ &l…