当前位置: 首页 > news >正文

C语言网络编程TCP通信实战:客户端↔服务器双向键盘互动全流程解析

在 Linux 下,基于 TCP 的客户端/服务器通信可以用 socket API 完成。本文通过三个示例讲解 C 语言 TCP 编程:

  1. 客户端发送一次消息到服务器(单向通信)
  2. 客户端键盘循环发送消息到服务器
  3. 客户端和服务器实现双向键盘循环通信

一、客户端发送一次消息到服务器(单向)

客户端代码

/*
整体流程(客户端):
1. 买电话(创建套接字)           fd = socket()
2. 绑卡(可选,一般不需要)        bind()
3. 打电话(发起连接)             connect()
4. 通话(收发数据)               send()/recv()
5. 挂电话(释放资源)             close()本程序:客户端向服务器发送一个字符串,服务器打印。
*/#include "net.h"int main(int argc, char const *argv[])
{int fd;int ret;// 1. 买电话:创建套接字// AF_INET  -> 使用 IPv4 协议族// SOCK_STREAM -> 使用面向连接的 TCP// 0 -> 协议自动选择(一般就是 TCP)fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1){perror("socket"); // 打印错误原因exit(1);          // 创建失败直接退出}/*2. 绑卡(客户端通常省略)bind() 是给 socket 强行指定“本机的 IP/端口”。- 一般情况下,客户端只需要关心对方(服务器)的 IP/端口;自己的 IP/端口由操作系统自动分配即可。- 如果客户端调用 bind(),可能会因为端口占用/冲突而导致 connect() 失败。所以常规客户端 **直接跳过 bind**。*/// 3. 打电话:connect 连接到服务器struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr("192.168.107.146"); // 服务器 IPserver.sin_port = htons(10001); // 服务器端口(主机字节序 → 网络字节序)ret = connect(fd, (struct sockaddr *)&server, sizeof(server));if (ret == -1){perror("connect");exit(1);}// 4. 发数据char buf[128] = "hello,world";ret = send(fd, buf, strlen(buf), 0);/*send 参数说明:- 第二个参数:要发送的数据- 第三个参数:要发送的字节数注意这里传 strlen(buf):- strlen(buf) 表示字符串的实际有效长度(不包含 '\0')。- 如果传 sizeof(buf)=128,就会把数组后面的 '\0' 也发过去,导致浪费带宽,甚至可能让对方收到一堆没用的空字符。总结:- 发数据时 → strlen(告诉内核“我实际要发多少”)- 收数据时 → sizeof(告诉内核“我最多能装多少”)*/if (ret == -1){perror("send");}// 5. 挂电话:释放套接字close(fd);return 0;
}

服务端代码

/*
整体流程(服务端):
1. 买电话(创建套接字)         fd = socket()
2. 绑卡(绑定服务器 IP/端口)   bind()
3. 开机(监听端口)             listen()
4. 等待来电(接受连接)         accept()
5. 通话(收发数据)             send()/recv()
6. 挂电话(释放资源)           close()本程序:服务器端接收客户端发来的字符串并打印。
*/#include "net.h"int main(int argc, char const *argv[])
{int fd;int ret;int new_fd;// 1. 买电话:创建套接字fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1){perror("socket");exit(1);}// 防止端口被占用:// 如果服务器异常退出,内核会在几分钟内保留端口(TIME_WAIT 状态),// 重新 bind() 会报“Address already in use”。// 所以要设置 SO_REUSEADDR,允许端口快速复用。// 客户端用处不大,一般是服务器才需要int opt = 1;setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 2. 绑卡:指定服务器的 IP 和端口struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr("192.168.107.146"); // 服务器 IPserver.sin_port = htons(10001); // 服务器端口ret = bind(fd, (struct sockaddr *)&server, sizeof(server));if (ret == -1){perror("bind");exit(1);}// 3. 监听:进入“接听模式”// backlog=5 表示内核允许排队的最大连接数为 5listen(fd, 5);printf("tcp server have started...\n");// 4. 接听:等待客户端呼入struct sockaddr_in client;socklen_t len = sizeof(client); // socklen_t 通常等于 unsigned int,(u32)new_fd = accept(fd, (struct sockaddr *)&client, &len);if (new_fd == -1){perror("accept");exit(1);}// 打印客户端信息printf("client connected: %s:%d\n",inet_ntoa(client.sin_addr), // IP 地址转字符串ntohs(client.sin_port));   // 端口转主机字节序// 5. 收数据char buf[128] = {0};ret = recv(new_fd, buf, sizeof(buf), 0);/*recv 参数说明:- 第二个参数:缓冲区- 第三个参数:最大能接收的字节数这里必须用 sizeof(buf),因为缓冲区还没有内容,没法用 strlen 来判断“能接收多少”。(strlen 只能用在已有字符串的场景)总结:- 收数据时 → sizeof(“最多能装多少”)- 发数据时 → strlen(“实际要发多少”)*/if (ret <= 0){perror("recv");close(new_fd);close(fd);return -1;}else{printf("receive from cli: %s\n", buf);}// 6. 挂电话// 先关通信套接字 new_fd,再关监听套接字 fd// 因为 监听套接字 fd 负责“接电话”,通信套接字 new_fd 负责“通话”// 先关 new_fd 表示先结束这次通话,再关 fd 表示整个电话服务不再提供// 如果反过来,先关 fd,监听功能就没了,但当前通话还在,逻辑上不合理close(new_fd);close(fd);return 0;
}

知识点

  • 客户端一次性发送消息
  • 服务端接收后打印,单向通信
  • strlen 用于发送数据长度,sizeof 用于接收缓冲区大小

二、客户端键盘循环发送消息到服务器

在实际应用中,客户端可能需要循环发送多条消息。此时可以用循环配合 fgets 获取用户输入。

客户端代码

/*
1. 买电话(创建套接字)            fd = socket()
2. 绑卡(可选,一般不需要)         bind()
3. 打电话(连接服务器)            connect()
4. 通话(循环发送数据)            send()
5. 挂电话(释放套接字)            close()本程序:客户端从键盘输入字符串,循环发送给服务器,直到输入 "quit"。
*/#include "net.h"int main(int argc, char *argv[])
{int fd;  // 客户端套接字int ret; // 系统调用返回值// 1. 买电话:创建 TCP 套接字fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1){perror("socket"); // 创建失败,打印错误原因exit(1);}// 允许重用端口(防止上次异常退出端口未释放)int on = 1;setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));/*2. 绑卡(客户端通常不写)bind() 用于指定本机 IP/端口。客户端不必写,操作系统会自动分配可用端口。写了 bind 可能与服务器端冲突,导致 connect 失败。*/// 3. 打电话:连接服务器struct sockaddr_in server;server.sin_family = AF_INET;                // IPv4 协议族server.sin_addr.s_addr = inet_addr("192.168.107.146"); // 服务器 IPserver.sin_port = htons(10001);            // 服务器端口(主机字节序→网络字节序)ret = connect(fd, (struct sockaddr *)&server, sizeof(server));if (ret == -1){perror("connect"); // 连接失败exit(1);}// 4. 通话:循环发送数据char buf[50] = {0};while (1){memset(buf, 0, 50); // 清空缓冲区,保证上次残留数据不会影响本次发送fgets(buf, 50, stdin); // 从键盘读取字符串// send 参数:// buf      -> 发送内容// strlen(buf) -> 实际要发的字节数(不含多余 '\0')ret = send(fd, buf, strlen(buf), 0);if (ret == -1){perror("send");}printf("send %d bytes\n", ret);// 输入 quit 即退出循环if (strncmp(buf, "quit\n", 5) == 0)break;}// 5. 挂电话:关闭套接字close(fd);return 0;
}

服务端代码

/*
1. 买电话(创建套接字)          fd = socket()
2. 绑卡(绑定服务器 IP/端口)     bind()
3. 开机监听(监听端口)          listen()
4. 等待来电(接受连接)          accept()
5. 通话(循环接收数据)          recv()
6. 挂电话(关闭套接字)          close()本程序:服务端接收客户端发送的字符串并打印,直到客户端发送 "quit" 或断开。
*/#include "net.h"int main(int argc, char *argv[])
{int fd;       // 监听套接字int ret;      // 系统调用返回值int new_fd;   // 通信套接字(每个客户端对应一个 new_fd)// 1. 买电话:创建 TCP 套接字fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1){perror("socket");exit(1);}// 防止端口被占用(上次异常退出可能导致 TIME_WAIT)int on = 1;setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));// 2. 绑卡:指定服务器 IP 和端口struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr("192.168.107.146"); // 本机 IPserver.sin_port = htons(10001);                         // 端口ret = bind(fd, (struct sockaddr *)&server, sizeof(server));if (ret == -1){perror("bind");exit(1);}// 3. 监听:进入监听模式// backlog = 5 表示内核排队的最大连接数listen(fd, 5);printf("tcp server have started...\n");// 4. 接听:等待客户端连接struct sockaddr_in client;socklen_t len = sizeof(client);new_fd = accept(fd, (struct sockaddr *)&client, &len);if (new_fd == -1){perror("accept");exit(1);}// 打印客户端信息printf("client connected:\nIP = %s\nPort = %d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));// 5. 通话:循环接收客户端数据char buf[50] = {0};while (1){memset(buf, 0, 50); // 清空缓冲区ret = recv(new_fd, buf, sizeof(buf), 0);// recv 参数:// buf      -> 接收缓冲区// sizeof(buf) -> 最大能接收的字节数printf("recv %d bytes: %s", ret, buf);// 客户端输入 quit 则退出if (strncmp(buf, "quit\n", 5) == 0){printf("client quit\n");break;}// 如果 recv 返回 0,表示客户端已经断开连接if (ret == 0){printf("client offline\n");break;}}// 6. 挂电话// 先关闭通信套接字 new_fd,再关闭监听套接字 fdclose(new_fd);close(fd);return 0;
}

知识点

  • 客户端可以循环发送消息
  • 服务端可以循环接收消息
  • 使用 memset 清空缓冲区,防止残留数据影响结果

三、客户端和服务器双向键盘循环通信

如果需要客户端和服务器同时可以发送和接收消息,需要 多线程 来实现收发分离。

客户端代码(双向循环)

#include "net.h"
#include <pthread.h>int fd; // 全局套接字,用于收发数据线程共享// 接收线程函数
void *recv_thread(void *arg)
{char buf[50];pthread_detach(pthread_self()); // 分离线程,避免主线程退出后线程僵死while (1) {memset(buf, 0, sizeof(buf));          // 清空缓冲区,防止上次残留数据影响本次接收int ret = recv(fd, buf, sizeof(buf), 0); // 从套接字接收数据if (ret <= 0) break;                  // 0表示对端关闭连接,<0表示错误printf("recv from server %d: %s", ret, buf); // 打印接收到的消息if (strncmp(buf, "quit\n", 5) == 0) break;   // 如果服务器发送 quit,退出循环}return NULL;
}int main()
{int ret;// 1. 创建 TCP 套接字fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1) { perror("socket"); exit(1); }// 2. 连接服务器struct sockaddr_in server;server.sin_family = AF_INET;                    // IPv4server.sin_addr.s_addr = inet_addr("192.168.107.146"); // 服务器 IPserver.sin_port = htons(10001);                // 服务器端口(大端)ret = connect(fd, (struct sockaddr *)&server, sizeof(server));if (ret == -1) { perror("connect"); exit(1); }// 3. 创建接收线程,专门接收服务器消息pthread_t tid;pthread_create(&tid, NULL, recv_thread, NULL);// 4. 主线程用于循环发送用户输入char buf[50];while (1) {memset(buf, 0, sizeof(buf));          // 清空缓冲区fgets(buf, sizeof(buf), stdin);       // 从键盘读取字符串ret = send(fd, buf, strlen(buf), 0);  // 发送实际长度的字符串printf("sent %d bytes\n", ret);       // 打印发送字节数if (strncmp(buf, "quit\n", 5) == 0) break; // 用户输入 quit,退出发送循环}// 5. 关闭套接字,释放资源close(fd);return 0;
}

服务端代码(双向循环)

#include "net.h"
#include <pthread.h>// 发送线程函数,用于循环发送消息给客户端
void *send_thread(void *arg)
{int newfd = *((int *)arg);          // 获取通信套接字char buf[50];pthread_detach(pthread_self());     // 分离线程while (1) {memset(buf, 0, sizeof(buf));    // 清空缓冲区fgets(buf, sizeof(buf), stdin); // 从键盘读取字符串int ret = send(newfd, buf, strlen(buf), 0); // 发送实际长度的字符串printf("sent %d bytes\n", ret);if (strncmp(buf, "quit\n", 5) == 0) break;  // 输入 quit,退出循环}return NULL;
}int main()
{int fd, newfd, ret;// 1. 创建 TCP 套接字(监听套接字)fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1) { perror("socket"); exit(1); }int on = 1;setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); // 允许重用端口,防止 bind 失败// 2. 绑定 IP 和端口struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr("192.168.107.146");server.sin_port = htons(10001);ret = bind(fd, (struct sockaddr *)&server, sizeof(server));if (ret == -1) { perror("bind"); exit(1); }// 3. 监听连接listen(fd, 5); printf("TCP server started...\n");// 4. 接受客户端连接struct sockaddr_in client;socklen_t len = sizeof(client);newfd = accept(fd, (struct sockaddr *)&client, &len);if (newfd == -1) { perror("accept"); exit(1); }printf("client connected: %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));// 5. 创建发送线程,用于键盘输入发送消息给客户端pthread_t tid;pthread_create(&tid, NULL, send_thread, &newfd);// 6. 主线程循环接收客户端消息char buf[50] = {0};while (1) {memset(buf, 0, sizeof(buf));            // 清空缓冲区ret = recv(newfd, buf, sizeof(buf), 0); // 接收客户端消息if (ret <= 0) break;                    // 对端关闭或错误,退出printf("recv from client %d: %s", ret, buf);if (strncmp(buf, "quit\n", 5) == 0) break; // 客户端发送 quit,退出}// 7. 关闭套接字close(newfd); // 先关闭通信套接字close(fd);    // 再关闭监听套接字return 0;
}

知识点

  • 双向通信需多线程分别处理收和发
  • pthread_detach 避免线程僵死
  • 循环发送/接收,输入 “quit” 可退出
  • memset 清空缓冲区,strlen/sizeof 用法正确

总结

  • 单向通信:客户端一次发送消息,服务器接收
  • 单向循环:客户端循环发送消息,服务器循环接收
  • 双向循环:客户端/服务器都可以同时发送和接收消息,多线程实现
  • 缓冲区处理:发送用 strlen(),接收用 sizeof()
  • 线程安全:尽量避免全局套接字,线程参数传递时注意生存周期
  • 资源管理:发送/接收完成后关闭套接字

(完)

http://www.xdnf.cn/news/1318735.html

相关文章:

  • 模拟实现 useEffect 功能
  • 【R语言】R 语言中打印含有双引号的字符串时会出现 “\” 的原因解析
  • 基于STM32单片机智能RFID刷卡汽车位锁桩设计
  • 基于51单片机汽车自动照明灯超声波光敏远近光灯设计
  • 自由学习记录(85)
  • TensorRT-LLM.V1.1.0rc0:在无 GitHub 访问权限的服务器上编译 TensorRT-LLM 的完整实践
  • 计算机网络 TCP time_wait 状态 详解
  • Java开发MCP服务器
  • thingsboard 服务器在2核CPU、2G内存资源配置下如何调优提速,适合开发/演示
  • vue封装请求拦截器 响应拦截器
  • 计算机网络 Session 劫持 原理和防御措施
  • 给纯小白的Python操作 PDF 笔记
  • 【算法】模拟专题
  • nertctl使用了解
  • B站 韩顺平 笔记 (Day 21)
  • Windows平台Frida逆向分析环境完整搭建指南
  • 机器学习05-朴素贝叶斯算法
  • 攻防世界—unseping(反序列化)
  • python的邮件发送及配置
  • 逆向Shell实战——红队技巧 vs 蓝队防御全攻略
  • Matlab数字信号处理——基于最小均方误差(MMSE)估计的自适应脉冲压缩算法复现
  • React 基础实战:从组件到案例全解析
  • Mysql笔记-错误条件\处理程序
  • 【Java后端】Spring Boot 集成 MyBatis 全攻略
  • 【前端基础】19、CSS的flex布局
  • 麒麟V10静默安装Oracle11g:lsnrctl、tnsping等文件大小为0的解决方案
  • 【编程实践】关于S3DIS数据集的问题
  • 官方正版在线安装office 365安装工具
  • react 错误边界
  • Linux系统分析 CPU 性能问题的工具汇总