网络编程 - - 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.rhkb.cn/news/2265.html

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

相关文章

近红外简单ROI分析matlab(NIRS_SPM)

本次笔记主要想验证上篇近红外分析是否正确&#xff0c;因为叠加平均有不同的计算方法&#xff0c;一种是直接将每个通道的5分钟实时长单独进行叠加平均&#xff0c;另一种是将通道划分为1分钟的片段&#xff0c;将感兴趣的通道数据进行对应叠加平均&#xff0c;得到一个总平均…

从玩具到工业控制--51单片机的跨界传奇【2】

咱们在上一篇博客里面讲解了什么是单片机《单片机入门》&#xff0c;让大家对单片机有了初步的了解。我们今天继续讲解一些有关单片机的知识&#xff0c;顺便也讲解一下我们单片机用到的C语言知识。如果你对C语言还不太了解的话&#xff0c;可以看看博主的C语言专栏哟&#xff…

Python调用go语言编译的库

要在 Python 中调用用 Go 语言编写的库&#xff0c;可以使用 Go 语言的 cgo 特性将 Go 代码编译成共享库&#xff08;如 .so 文件&#xff09;&#xff0c;然后在 Python 中通过 ctypes 或 cffi 模块加载和调用这个共享库。 新建main.go文件&#xff0c;使用go语言编写如下代码…

学成在线_内容管理模块_创建模块工程

学成在线模块工程 1.各个微服务依赖基础工程2.每个微服务都是一个前后端分离的项目3.xuecheng-plus-content&#xff1a;内容管理模块工程xuecheng-plus-content-modelxuecheng-plus-content-servicexuecheng-plus-content-api 1.各个微服务依赖基础工程 2.每个微服务都是一个前…

1️⃣Java中的集合体系学习汇总(List/Map/Set 详解)

目录 01. Java中的集合体系 02. 单列集合体系​ 1. Collection系列集合的遍历方式 &#xff08;1&#xff09;迭代器遍历&#xff08;2&#xff09;增强for遍历​编辑&#xff08;3&#xff09;Lambda表达式遍历 03.List集合详解 04.Set集合详解 05.总结 Collection系列…

智能科技与共情能力加持,哈曼重新定义驾乘体验

2025年1月6日&#xff0c;拉斯维加斯&#xff0c;2025年国际消费电子展——想象一下&#xff0c;当您步入一辆汽车&#xff0c;它不仅能响应您的指令&#xff0c;更能理解您的需求、适应您的偏好&#xff0c;并为您创造一个独特且专属的交互环境。作为汽车科技领域的知名企业和…

Unity中实现倒计时结束后干一些事情

问题描述&#xff1a;如果我们想实现在一个倒计时结束后可以执行某个方法&#xff0c;比如挑战成功或者挑战失败&#xff0c;或者其他什么的比如生成boss之类的功能&#xff0c;而且你又不想每次都把代码复制一遍&#xff0c;那么就可以用下面这种方法。 结构 实现步骤 创建一…

从0开始学习搭网站第二天

前言&#xff1a;今天比较惭愧&#xff0c;中午打铲吃了一把&#xff0c;看着也到钻二了&#xff0c;干脆顺手把这个赛季的大师上了&#xff0c;于是乎一直到网上才开始工作&#xff0c;同样&#xff0c;今天的学习内容大多来自mdn社区mdn 目录 怎么把文件上传到web服务器采用S…

STM32 FreeRTOS时间片调度---FreeRTOS任务相关API函数---FreeRTOS时间管理

目录 时间片调度简介 FreeRTOS任务相关API函数介绍 延时函数介绍 时间片调度简介 在FreeRTOS中&#xff0c;同等优先级的任务会轮流分享相同的CPU时间&#xff0c;这个时间被称为时间片。在这里&#xff0c;一个时间片的长度等同于SysTick中断的周期。 FreeRTOS任务相关API…

VM(虚拟机)和Linux的安装

文章目录 1.虚拟机1.1 VM的安装和删除1.1.1 安装前提1.1.2 安装步骤 1.2 虚拟机快照1.3 虚拟机的克隆 2.Linux的安装2.1 CentOS2.2 Ubuntu 1.虚拟机 &#xff08;1&#xff09;Linux系统的安装方式 ①物理机安装&#xff1a;直接将操作系统安装到服务器硬件上 ②虚拟机安装&am…

C++算法第十五天

复习周终于结束了&#xff0c;这也是复习周结束后的第一篇文章&#xff0c;请各位小伙伴们细细品尝&#xff0c;废话不多说&#xff0c;我们开始今天的讲解。 第一题 题目链接 918. 环形子数组的最大和 - 力扣&#xff08;LeetCode&#xff09; 题目解析 代码原理 注意&…

mysql-5.7.18保姆级详细安装教程

本文主要讲解如何安装mysql-5.7.18数据库&#xff1a; 将绿色版安装包mysql-5.7.18-winx64解压后目录中内容如下图&#xff0c;该例是安装在D盘根目录。 在mysql安装目录中新建my.ini文件&#xff0c;文件内容及各配置项内容如下图&#xff0c;需要先将配置项【skip-grant-tab…

<OS 有关>Ubuntu 24 安装 openssh-server, tailscale+ssh 慢增加

更新日志&#xff1a; Created on 14Jan.2025 by Dave , added openssh-server, tailescape Updated on 15Jan.2025, added "tailescape - tailscape ssh" 前期准备&#xff1a; 1. 更新可用软件包的数据库 2. 升级系统中所有已安装的软件包到最新版本 3. 安装 cur…

STM32-keil安装时遇到的一些问题以及解决方案

前言&#xff1a; 本人项目需要使用到STM32,故需配置keil 5&#xff0c;在配置时遇到了以下问题&#xff0c;并找到相应的解决方案&#xff0c;希望能够为遇到相同问题的道友提供一些解决思路 1、提示缺少&#xff08;missing&#xff09;version 5编译器 step1&#xff1a;找…

HTTP1.0/1.1/2.0/3.0 的区别?

HTTP&#xff08;Hypertext Transfer Protocol&#xff09;是用于传输超文本的协议。各版本的主要区别体现在性能优化、数据传输方式以及支持的功能上。 每一次协议的更新都是对旧协议的改进&#xff1a; 1. HTTP1.0 发布于1996年 无连接&#xff08;Connectionless&#…

蓝桥杯_B组_省赛_2022(用作博主自己学习)

题目链接算法11.九进制转十进制 - 蓝桥云课 进制转换 21.顺子日期 - 蓝桥云课 时间与日期 31.刷题统计 - 蓝桥云课 时间与日期 41.修剪灌木 - 蓝桥云课 思维 51.X 进制减法 - 蓝桥云课 贪心 61.统计子矩阵 - 蓝桥云课 二维前缀和 71.积木画 - 蓝桥云课 动态规划 82.扫雷 - 蓝桥…

C++|CRC校验总结

参考&#xff1a; Vector - CAPL - CRC算法介绍 开发工具 > CRC校验工具 文章目录 简介CRC-8CRC-16CRC-32 简介 循环冗余校验&#xff08;Cyclic Redundancy Check&#xff0c;简称CRC&#xff09;是一种数据校验算法&#xff0c;广泛用于检测数据传输或存储过程中的错误。…

迅翼SwiftWing | ROS 固定翼开源仿真平台正式发布!

经过前期内测调试&#xff0c;ROS固定翼开源仿真平台今日正式上线&#xff01;现平台除适配PX4ROS环境外&#xff0c;也已实现APROS环境下的单机飞行控制仿真适配。欢迎大家通过文末链接查看项目地址以及具体使用手册。 1 平台简介 ROS固定翼仿真平台旨在实现固定翼无人机决策…

C语言数据结构与算法(排序)详细版

大家好&#xff0c;欢迎来到“干货”小仓库&#xff01;&#xff01; 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;无人扶我青云志&#xff0c;我自踏雪至山巅&#xff01;&#xff01;&am…

微信小程序获取openid

2025年1月15日&#xff1a; 注意&#xff1a;其中appid,secret&#xff0c;还有服务器网址都按自己实际的填写 1、先在云服务器上安装nodejs&#xff0c;然后写个get接口&#xff1a; const express require(express); const app express();app.get(/getOpenid,(req,res)&…