实现简单的TCP服务器

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

这里我们选择2048

先在ubuntu系统中输入netstat -anop | grep 2048,会显示以下信息

这代表此时2048端口号没有被占用

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

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. 创建套接字

    c 复制代码
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

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

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

    c 复制代码
    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. 绑定套接字到地址

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

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

  4. 监听客户端连接

    c 复制代码
    if (listen(sockfd, 10) == -1) {
        perror("listen");
        return -1;
    }

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

  5. 接受客户端连接

    c 复制代码
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);

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

  6. 接收和发送数据

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

    c 复制代码
    close(clientfd);

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

编写完成后我们进行编译和运行

此时程序处于运行中

我们在系统再运行一次netstat -anop | grep 2048显示

可以看到此时2048端口处于监听状态

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

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

点击连接

c程序这里也显示连接到了

再试一次netstat -anop | grep 2048

可以看到显示有一个客户端连接到了

这时我们在网络调试助手发送消息

c程序也接受到了消息并返回了相应的信息

但这个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. 包含头文件

c 复制代码
#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. 定义常量

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

3. handle_client 函数

c 复制代码
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* 类型的指针,指向传递给线程的参数。在这个例子中,传递的是客户端套接字的描述符。
  • 接收客户端数据

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

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

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

4. main 函数

c 复制代码
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. 创建服务器套接字
c 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • 通过 socket() 创建一个 TCP 套接字。AF_INET 表示使用 IPv4 地址族,SOCK_STREAM 表示使用 TCP 协议(流式套接字)。

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

2. 配置服务器地址
c 复制代码
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. 绑定套接字到指定的地址和端口
c 复制代码
bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
  • bind() 函数将服务器的套接字 sockfdserveraddr 中存储的地址和端口绑定。
4. 开始监听端口
c 复制代码
listen(sockfd, MAX_CLIENTS);
  • listen() 函数使套接字进入监听模式,MAX_CLIENTS 定义了最多可以排队等待的客户端连接数。
5. 接受客户端连接
c 复制代码
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
  • accept() 函数接受一个客户端连接请求,返回一个新的套接字 clientfd 用于与客户端进行通信。客户端的地址信息保存在 `

clientaddr` 中。

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

总结:

  • 该代码实现了一个多线程的 TCP 服务器,能够同时处理多个客户端连接。
  • 每当有一个新的客户端连接,服务器创建一个新的线程来处理该连接,避免了串行处理多个客户端带来的延迟。
相关推荐
丿无缘梦丨14 分钟前
ubuntu,vmware虚拟机共享文件夹配置
linux·ubuntu
azoon.top16 分钟前
centos设置开机自启的几种方案(frp为例)
linux·运维·服务器·centos·frp·开机自启
keep intensify36 分钟前
c语言分支和循环
c语言·开发语言·算法
0xCC说逆向1 小时前
Windows图形界面(GUI)-QT-C/C++ - QT 窗口属性
c语言·开发语言·c++·windows·qt·mfc
m0_748240021 小时前
CentOS部署FastDFS+Nginx并实现远程访问本地服务器中文件
服务器·nginx·centos
小仇学长1 小时前
Linux内核编程(二十一)USB驱动开发
linux·驱动开发·usb
m0_748254091 小时前
Linux下MySQL的简单使用
linux·mysql·adb
lsx1_231 小时前
[c]可变参数函数
c语言·c++
qq_333617241 小时前
OSPF的LSA的学习研究
网络·学习
Cici_ovo1 小时前
什么是IP地址、子网掩码、网关、DNS
网络