网络编程 - - TCP套接字通信及编程实现

概述

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的传输层协议。在网络编程中,TCP常用于实现客户端和服务器之间的可靠数据传输。本文将基于C语言实现TCP服务端和客户端建立通信的过程。

三次握手

在 TCP 连接建立之前,客户端和服务器之间需要进行三次握手来同步双方的序列号,并确认双方都准备好进行数据传输

  • 第一次握手:客户端向服务器发送一个 SYN(同步序列编号)报文段,表示请求建立连接。客户端进入 SYN_SENT 状态
  • 第二次握手:服务器收到 SYN 报文段后,回复一个 SYN-ACK(同步序列编号 + 确认)报文段,表示同意建立连接。服务器进入 SYN_RCVD 状态
  • 第三次握手:客户端收到 SYN-ACK 报文段后,回复一个 ACK(确认)报文段,表示确认收到服务器的响应。客户端和服务器都进入 ESTABLISHED 状态,连接正式建立

在这里插入图片描述

图片来源:https://img-blog.csdnimg.cn/39bb4f4da21a4513b9506ecdf6a40cf3.png

四次挥手

通信结束时,客户端或服务器可以发起断开连接的请求。断开连接的过程称为四次挥手,以确保双方都能正确关闭连接并释放资源

  • 第一次挥手:主动关闭方(通常是客户端)发送一个 FIN(终止)报文段,表示不再发送数据。主动关闭方进入 FIN_WAIT_1 状态
  • 第二次挥手:被动关闭方(通常是服务器)收到 FIN 报文段后,回复一个 ACK 报文段,表示确认收到 FIN。被动关闭方进入 CLOSE_WAIT 状态,而主动关闭方进入 FIN_WAIT_2 状态
  • 第三次挥手:被动关闭方在处理完所有未完成的数据后,发送一个 FIN 报文段,表示自己也不再发送数据。被动关闭方进入 LAST_ACK 状态
  • 第四次挥手:主动关闭方收到 FIN 报文段后,回复一个 ACK 报文段,表示确认收到 FIN。主动关闭方进入 TIME_WAIT 状态,等待一段时间(通常为2倍的最大报文段生命周期,即2MSL),以确保被动关闭方收到了最后的 ACK。之后,主动关闭方进入 CLOSED 状态,连接完全关闭

2MSL:MSL 的默认值是 30 秒,基于经验选择的一个保守估计,用来确保大多数网络环境下的数据包都能被接收或者超时。

大部分操作系统都允许用户调整 MSL 的值,从而改变 TIME_WAIT 状态的持续时间

在这里插入图片描述

图片来源:https://i-blog.csdnimg.cn/blog_migrate/843f121dd50cd8458daf1fa834bc1f36.png

TCP保证可靠传输方式

  • 序列号:每个TCP报文段都有一个序列号,表示该报文段中的第一个字节在整个数据流中的位置。接收方可以根据序列号重新排序接收到的报文段,确保数据按顺序传递
  • 确认应答:接收方在收到报文段后,会发送一个确认应答,告诉发送方哪些数据已经成功接收。发送方根据确认应答判断是否需要重传丢失或损坏的报文段
  • 超时重传:如果发送方在一定时间内没有收到确认应答,它会认为报文段可能丢失或延迟,并重新发送该报文段。TCP使用动态调整的超时机制来优化重传策略
  • 流量控制:TCP使用滑动窗口机制来控制发送方的发送速率,确保接收方不会被过多的数据淹没。接收方会在确认应答中告知发送方当前可用的接收窗口大小,发送方根据这个信息调整自己的发送速率
  • 拥塞控制:TCP通过多种算法(如慢启动、拥塞避免、快速重传和快速恢复)来动态调整发送方的发送速率,避免网络拥塞。这些算法旨在在网络负载较高时减小发送速率,在网络条件改善时逐渐增加发送速率

TCP通信实现流程

涉及到的库方法

  • 创建套接字(Socket)

    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
    
  • 绑定(Bind)套接字到指定的IP地址和端口

    #include <sys/socket.h>
    int bind(int socket, const struct sockaddr *address, socklen_t address_len);
    
  • 监听(Listen)客户端的连接请求

    #include <sys/socket.h>
    int listen(int socket, int backlog)
    
  • 接受(Accept)客户端的连接

    #include <sys/socket.h>
    int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
    
  • 连接、发送和接收数据(Send/Recv)

    #include <sys/socket.h>
    int connect(int socket, const struct sockaddr *address, socklen_t address_len);
    ssize_t send(int socket, const void *buffer, size_t length, int flags);
    ssize_t recv(int socket, void *buffer, size_t length, int flags);
    
  • 关闭连接(Close)

    #include <unistd.h>
    int close(int fildes);
    

代码实现

服务器代码(Linux)

tcp_server.h

#ifndef TCP_SERVER_H
#define TCP_SERVER_H#include <pthread.h>
#include <netinet/in.h>
#include "cJson.h"
#include "common_base.h"// 定义常量
#define PORT 18888
#define BUFFER_SIZE 1024
#define TCP_IP "127.0.0.1"// 外部函数声明
extern void getSerialNoStr(char *buf);/*** 启动服务器主循环,等待客户端连接*/
void start_serve_tcp(void *arg);/*** 解析接收到的消息
*/
void parse_message(char *data, size_t data_size);#endif // TCP_SERVER_H

tcp_server.c

#include "tcp_server.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>/*** 启动TCP服务器,等待客户端连接并处理接收到的数据。** @param arg 传递给线程的参数,通常为NULL。*/
void start_serve_tcp(void *arg) 
{int server_fd, client_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);char buffer[BUFFER_SIZE] = {0};// 创建 Socketif ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){printf("Socket 创建失败");return;}// 配置服务器地址memset_s(&server_addr, sizeof(server_addr), 0, sizeof(server_addr));server_addr.sin_family = AF_INET;// 配置端口号和IPserver_addr.sin_port = htons(PORT);server_addr.sin_addr.s_addr = inet_addr(TCP_IP);// 绑定 Socketif (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {printf("绑定失败\n");close(server_fd);return;}// 开始监听if (listen(server_fd, 5) == -1){printf("监听失败\n");close(server_fd);return;}printf("服务器已启动,监听地址:%s:%d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));// 等待客户端连接while (1){printf("等待客户端连接...\n");if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len)) == -1) {printf("接受客户端连接失败");continue;}printf("客户端已连接:%s:%d\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));// 接收数据int received_size;printf("receive before client_fd: %d\n", client_fd);while ((received_size = recv(client_fd, buffer, BUFFER_SIZE, 0)) > 0) {printf("收到数据,大小: %d 字节\n", received_size);parse_message(buffer, received_size);printf("jsonString: %s\n", jsonString);sleep(1);if(jsonString){              printf("receive after client_fd: %d\n", client_fd);// 发送 JSON 数据到客户端ssize_t sent_size = send(client_fd, jsonString, strlen(jsonString), 0);if (sent_size == -1){printf("发送数据失败\n");} else {printf("成功发送 %zd 字节到客户端\n", sent_size);}free(jsonString);jsonString = NULL;}else {printf("生成 JSON 数据失败\n");}}if (received_size == 0){printf("客户端已断开连接\n");} else if (received_size == -1){printf("接收数据失败");}close(client_fd);}close(server_fd);
}void parse_message(char *data, size_t data_size) 
{cJSON *rootMsg = NULL;  cJSON *serialNo = NULL; // 序列号cJSON *netCmd = NULL;   // 操作行为MANUAL_TRIG_PARAM manualTrParam;    // 手动触发抓拍接口传参结构体// 确保消息头部完整性(消息类型:4字节,数据长度:4字节)if (data_size < 8){printf("数据长度不足,无法解析\n");return;}// 解析消息类型和数据长度int message_type = ntohl(*(int *)data);int data_length = ntohl(*(int *)(data + 4));printf("消息类型: %d\n", message_type);printf("数据长度: %d\n", data_length);// 检查数据长度是否匹配if (data_size < 8 + data_length) {printf("数据长度与实际内容不匹配\n");return;}// 解析消息内容char *message_content = (char *)malloc(data_length + 1);if (!message_content) {printf("内存分配失败\n");return;}memcpy(message_content, data + 8, data_length);message_content[data_length] = '\0'; // 确保字符串以 \0 结尾printf("消息内容: %s\n", message_content);rootMsg = cJSON_Parse(message_content);if (NULL == rootMsg){printf("rootMsg is not json tpye\n");free(message_content);return;}printf("解析成功\n");serialNo = cJSON_GetObjectItem(rootMsg, "serialNo");netCmd = cJSON_GetObjectItem(rootMsg, "netCmd");if (serialNo && netCmd){printf("serialNo: %s\n", serialNo->valuestring);printf("netCmd: %s\n", netCmd->valuestring);} else{printf("缺少必要的 JSON 字段\n");cJSON_Delete(rootMsg);free(message_content);return;}char serial[64] = {0};getSerialNoStr(serial);printf("getSerialNoStr: %s\n", serial);printf("serialNo: %s\n", serialNo->valuestring);if(strncmp(serial, serialNo->valuestring, 9) == 0){// TODO:业务处理}else{printf("serialNo is not equal\n");}cJSON_Delete(rootMsg);free(message_content);
}
客户端代码(Windows)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>#pragma comment(lib, "ws2_32.lib")void send_message(const char *ip, int port, int message_type, const char *content) {WSADATA wsa;SOCKET sock;struct sockaddr_in server_addr;char buffer[1024];int recv_size;char recv_buffer[1024];// 初始化 Winsockif (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {printf("Winsock 初始化失败,错误代码: %d\n", WSAGetLastError());exit(EXIT_FAILURE);}// 创建 Socketif ((sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {printf("Socket 创建失败,错误代码: %d\n", WSAGetLastError());WSACleanup();exit(EXIT_FAILURE);}// 配置服务器地址server_addr.sin_family = AF_INET;server_addr.sin_port = htons(port);server_addr.sin_addr.s_addr = inet_addr(ip);// 连接服务器if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {printf("连接服务器失败,错误代码: %d\n", WSAGetLastError());closesocket(sock);WSACleanup();exit(EXIT_FAILURE);}// 准备消息int data_length = strlen(content);int net_message_type = htonl(message_type);int net_data_length = htonl(data_length);memcpy(buffer, &net_message_type, 4);memcpy(buffer + 4, &net_data_length, 4);memcpy(buffer + 8, content, data_length);// 发送消息send(sock, buffer, 8 + data_length, 0);// 接收服务器返回的数据if ((recv_size = recv(sock, recv_buffer, sizeof(recv_buffer) - 1, 0)) == SOCKET_ERROR) {printf("接收数据失败,错误代码: %d\n", WSAGetLastError());} else {recv_buffer[recv_size] = '\0'; // 确保以 null 结尾printf("服务器返回: %s\n", recv_buffer);}closesocket(sock);WSACleanup();
}int main() {send_message("127.0.0.1", 18888, 1, "{id: 'FS123456', command: 'NET_CAP'}");return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/66212.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023-2024 学年 广东省职业院校技能大赛(高职组)“信息安全管理与评估”赛题一

2023-2024 学年 广东省职业院校技能大赛(高职组“信息安全管理与评估”赛题一&#xff09; 模块一:网络平台搭建与设备安全防护第一阶段任务书任务 1&#xff1a;网络平台搭建任务 2&#xff1a;网络安全设备配置与防护DCRS:DCFW:DCWS:DCBC:WAF: 模块二&#xff1a;网络安全事件…

thinkphp6 + redis实现大数据导出excel超时或内存溢出问题解决方案

redis下载安装&#xff08;window版本&#xff09; 参考地址&#xff1a;https://blog.csdn.net/Ci1693840306/article/details/144214215 php安装redis扩展 参考链接&#xff1a;https://blog.csdn.net/jianchenn/article/details/106144313 解决思路&#xff1a;&#xff0…

PT8M2302 触控 A/D 型 8-Bit MCU

1. 产品概述 PT8M2302 是一款可多次编程&#xff08; MTP &#xff09; A/D 型 8 位 MCU &#xff0c;其包括 2K*16bit MTP ROM 、 256*8bit SRAM、 ADC 、 PWM 、 Touch 等功能&#xff0c;具有高性能精简指令集、低工作电压、低功耗特性且完全集 成触控按键功能。为…

Nvidia Blackwell架构深度剖析:深入了解RTX 50系列GPU的升级

在CES 2025上&#xff0c;英伟达推出了基于Blackwell架构的GeForce RTX 50系列显卡&#xff0c;包括RTX 5090、RTX 5080、RTX 5070 Ti和RTX 5070。一段时间以来&#xff0c;我们已经知晓了该架构的各种细节&#xff0c;其中许多此前还只是传闻。不过&#xff0c;英伟达近日在20…

计算机网络 (45)动态主机配置协议DHCP

前言 计算机网络中的动态主机配置协议&#xff08;DHCP&#xff0c;Dynamic Host Configuration Protocol&#xff09;是一种网络管理协议&#xff0c;主要用于自动分配IP地址和其他网络配置参数给连接到网络的设备。 一、基本概念 定义&#xff1a;DHCP是一种网络协议&#xf…

RV1126+FFMPEG推流项目(7)AI音频模块编码流程

一、AI 模块和外设麦克风的关系 AI 模块是 RV1126 芯片的一个重要组成部分。它的主要功能是将外部接入的麦克风采集到的模拟信号通过内置的驱动程序转换为数字信号。这意味着麦克风作为外设&#xff0c;提供音频输入信号&#xff0c;AI 模块通过其硬件和软件的结合&#xff0c…

第十二章:算法与程序设计

文章目录&#xff1a; 一&#xff1a;基本概念 1.算法与程序 1.1 算法 1.2 程序 2.编译预处理 3.面向对象技术 4.程序设计方法 5.SOP标志作业流程 6.工具 6.1 自然语言 6.2 流程图 6.3 N/S图 6.4 伪代码 6.5 计算机语言 二&#xff1a;程序设计 基础 1.常数 …

【k8s面试题2025】2、练气初期

在练气初期&#xff0c;灵气还比较稀薄&#xff0c;只能勉强在体内运转几个周天。 文章目录 简述k8s静态pod为 Kubernetes 集群移除新节点&#xff1a;为 K8s 集群添加新节点Kubernetes 中 Pod 的调度流程 简述k8s静态pod 定义 静态Pod是一种特殊类型的Pod&#xff0c;它是由ku…

初学stm32 --- CAN

目录 CAN介绍 CAN总线拓扑图 CAN总线特点 CAN应用场景 CAN物理层 CAN收发器芯片介绍 CAN协议层 数据帧介绍 CAN位时序介绍 数据同步过程 硬件同步 再同步 CAN总线仲裁 STM32 CAN控制器介绍 CAN控制器模式 CAN控制器模式 CAN控制器框图 发送处理 接收处理 接收过…

运输层安全协议SSL

安全套接字层 SSL (Secure Socket Layer) SSL 作用在端系统应用层的 HTTP 和运输层之间&#xff0c;在 TCP 之上建立起一个安全通道&#xff0c;为通过 TCP 传输的应用层数据提供安全保障。 应用层使用 SSL 最多的就是 HTTP&#xff0c;但 SSL 并非仅用于 HTTP&#xff0c;而是…

【Sql递归查询】Mysql、Oracle、SQL Server、PostgreSQL 实现递归查询的区别与案例(详解)

文章目录 Mysql 5.7 递归查询Mysql 8 实现递归查询Oracle递归示例SQL Server 递归查询示例PostgreSQL 递归查询示例 更多相关内容可查看 Mysql 5.7 递归查询 MySQL 5.7 本身不直接支持标准 SQL 中的递归查询语法&#xff08;如 WITH RECURSIVE 这种常见的递归查询方式&#xf…

【Rust自学】13.2. 闭包 Pt.2:闭包的类型推断和标注

13.2.0. 写在正文之前 Rust语言在设计过程中收到了很多语言的启发&#xff0c;而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。 在本章中&#xff0c;我们会讨论 Rust 的一…

【JavaScript】比较运算符的运用、定义函数、if(){}...esle{} 语句

比较运算符 !><> < 自定义函数&#xff1a; function 函数名&#xff08;&#xff09;{ } 判断语句&#xff1a; if(判断){ }else if(判断){ 。。。。。。 }else{ } 代码示例&#xff1a; <!DOCTYPE html> <html> <head><meta charset&quo…

WOA-Transformer鲸鱼算法优化编码器时间序列预测(Matlab实现)

WOA-Transformer鲸鱼算法优化编码器时间序列预测&#xff08;Matlab实现&#xff09; 目录 WOA-Transformer鲸鱼算法优化编码器时间序列预测&#xff08;Matlab实现&#xff09;预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现WOA-Transformer鲸鱼算法优化编…

25/1/15 嵌入式笔记 初学STM32F108

GPIO初始化函数 GPIO_Ini&#xff1a;初始化GPIO引脚的模式&#xff0c;速度和引脚号 GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化GPIOA的引脚0 GPIO输出控制函数 GPIO_SetBits&#xff1a;将指定的GPIO引脚设置为高电平 GPIO_SetBits(GPIOA, GPIO_Pin_0); // 将GPIO…

mac m4 安装 node

brew install node // 安装 node //安装的路径在&#xff1a; /opt/homebrew/bin/node brew install node14 // brew install node22 // 安装指定版本 如果需要设置环境变量&#xff1a;通过&#xff1a; which node 查找路径 export PATH"/usr/local/opt/…

haproxy+nginx网站架构,实现负载均衡实验笔记

前提准备&#xff1a; 两台nginx&#xff0c;一台haproxynginx1&#xff1a;192.168.180.120nginx2&#xff1a;192.168.180.130&#xff0c;NFShaproxy&#xff1a;192.168.180.110 nginx&#xff08;两台nginx的操作是一样的&#xff09;&#xff1a; 1. 安装nginx #先安…

【C++篇】红黑树的实现

目录 前言&#xff1a; 一&#xff0c;红黑树的概念 1.1&#xff0c;红黑树的规则 1.2&#xff0c;红黑树的最长路径 1.3&#xff0c;红黑树的效率分析 二&#xff0c;红黑树的实现 2.1&#xff0c;红黑树的结构 2.2&#xff0c;红黑树的插入 2.2.1&#xff0c;大致过程…

如何在谷歌浏览器中设置自定义安全警告

随着网络环境的日益复杂&#xff0c;浏览器的安全问题也愈发引人关注。谷歌浏览器作为一款广泛使用的浏览器&#xff0c;其自定义安全警告功能为用户提供了更加个性化和安全的浏览体验。本文将详细介绍如何在谷歌浏览器中设置自定义安全警告&#xff0c;帮助用户更好地保护自己…

Spring 6 第1章——概述

一.Spring是什么 Spring是一款主流的Java EE轻量级&#xff08;体积小、不需要依赖其它组件&#xff09;开源框架Spring的目的是用于简化Java企业级应用的开发难度和开发周期Spring的用途不仅限于服务端的开发&#xff0c;从简单性、可测试性和松耦合的角度而言&#xff0c;任…