一、服务端程序

服务端程序工作流程: 创建socket →\rightarrow→ 绑定监听的IP地址和端口 →\rightarrow→ 监听客户端连接 →\rightarrow→ 接受/发送数据。对应到系统API的调用就是socket() →\rightarrow→ bind() →\rightarrow→ listen() →\rightarrow→ accept() →\rightarrow→ recv()/send()

socket()创建socket,返回一个文件描述符,这个文件描述符只用于监听客户端的连接,不负责与客户端通信。调用accept()函数后,会返回一个与当前客户端通信的文件描述符。

TCP建立连接的过程: 调用listen()后,服务器监听指定的端口。收到客户端发来的SYN报文后(第一次握手),将这个连接请求放入半连接队列,并返回SYN+ACK报文(第二次握手)。当收到客户端发来的ACK报文后(第三次握手),服务器将这个连接请求从半连接队列中拿出,放入全连接队列中,等待accpet()取出。

listen()和accpet()的区别: 调用listen()后就已经可以与客户端建立连接了。accpet()只是从全连接队列中拿出一个已经建立的连接

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>int open_socket() {int sockfd = 0;if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {perror("socket wrong\n");}// 在 struct sockaddr_in 中设置监听信息,三个属性:// 1. sin_family:协议类型// 2. sin_port:端口号// 3. sin_addr:一个struct,其中只包含一个 unsigned long 字段 s_addr,存储 ip 地址struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;// INADDR_ANY:监听所有网卡的 ip 地址server_addr.sin_addr.s_addr = htonl(INADDR_ANY);// htons: 将主机字节序(通常为小端)转换为网络字节序(大端)server_addr.sin_port = htons(5005);if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) != 0) {perror("bind wrong\n");}if (listen(sockfd, 4096) != 0) {perror("listen wrong\n");}return sockfd;
}int main() {int sockfd = open_socket();int clientfd = 0;struct sockaddr_in client_addr;int size = sizeof(struct sockaddr_in);clientfd = accept(sockfd, (struct sockaddr*)&client_addr, (socklen_t*)&size);char buffer[1024];int ret = 0;while (true) {memset(buffer, 0, sizeof(buffer));ret = recv(clientfd, buffer, sizeof(buffer), 0);if (ret == 0) {printf("client disconnect\n");break;} else if (ret < 0) {perror("recv error\n");} else {printf("msg size = %d, msg = %s\n", ret, buffer);}}close(sockfd);close(clientfd);return 0;
}

全连接队列满了会怎样: listen()的第二个参数由用户指定全连接队列最大长度,但实际长度由这个参数和内核参数共同决定。实际长度=min(backlog, /proc/sys/net/core/somaxconn)。backlog约等于listen()的第二个参数,但略有不同。比如我指定backlog = 2,实际的全连接队列最大长度为3。

当全连接队列满后,服务端不会再接受第三次握手的ACK报文,一定时间后会重发第二次握手的SYN+ACK报文,因此没有进入全连接队列的客户端会回到SYN_SENT状态并重新发送ACK报文,直到超时。比如服务端程序不进行accept(),启动10个客户端连接,用ss -nt命令查看连接状态,发现只有三个客户端可以进入ESTABLISH状态,其它客户端处于SYN_SENT状态。

二、客户端程序

客户端工作流程: 创建socket →\rightarrow→ 连接服务端 →\rightarrow→ 接受/发送数据

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int open_connect(const char* ip, uint16_t port) {int sockfd;if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {perror("socket wrong\n");return sockfd;}struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));// struct hostent:保存 ip 地址,以网络字节序存储struct hostent* h = NULL;// gethostbyname:将 ip 地址或域名转化为网络字节序,返回一个 struct hostent*if ((h = gethostbyname(ip)) == NULL) {printf("wrong host name\n");}memcpy(&server_addr.sin_addr, h->h_addr, h->h_length);server_addr.sin_family = AF_INET;server_addr.sin_port = htons(port);if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) != 0) {perror("connect wrong\n");}return sockfd;
}
int main() {int sockfd = open_connect("127.0.0.1", 5005);char buffer[1024];int ret;for (int i = 0; i < 1000; i++) {memset(buffer, 0, sizeof(buffer));sprintf(buffer, "this is message %d", i);// strlen(buffer) + 1 是额外计算'\0'所占的内存if ((ret = send(sockfd, buffer, strlen(buffer) + 1, 0)) <= 0) {perror("send error\n");}printf("send message: %s\n", buffer);}close(sockfd);return 0;
}

send()函数: 调用send()函数后,会将数据拷贝到内核缓冲区中(只拷贝,不负责真的发送)。如果要发送的数据超过了缓冲区大小,则返回错误。如果发送数据超过了缓冲区当前剩余大小,则只拷贝缓冲区能容下的部分数据。

三、TCP粘包

上述程序运行后,服务端接收到的数据为:

客户端每次发送18或19个字节的数据,即字符串“this is message xx”,但服务端每次接受到的数据长度不一样,说明有多条数据拼接在一起(实际打印的数据会截断到’\0’的位置),这就是TCP粘包问题。

TCP粘包: TCP是面向字节流的协议,一个TCP报文中携带的数据并不由应用程序决定。上面的客户端程序每次send一个字符串到内核缓冲区,但每次send的数据不一定是一个TCP报文,一个报文中可能包含多个send过来的数据。服务端在读取时,会在buffer大小范围内,将缓冲区的数据全都读进来。

解决TCP粘包问题: 每次发送数据之前,先发送一个固定长度的自定义包头,包头中定义了这一次数据的长度。服务端先按照包头长度接受包头数据,再按照包头数据中指定的数据长度接受这一次通信的数据。

在下面代码中,我们使用一个int类型作为“包头”,代表发送数据的长度。而int类型固定4字节,因此服务端每次先接受4字节的数据x,再接受x字节的字符串数据。

// 替代 recv() 函数
bool receive_message(int clientfd, char* buf) {int one_size = 0;int msg_size = 0;// 先接受 msg_sizeone_size = recv(clientfd, &msg_size, sizeof(int), 0);if (one_size == 0) {printf("client disconnect\n");return false;}if (one_size < 0) {perror("recv wrong\n");return false;}int pos = 0;while (msg_size > 0) {one_size = recv(clientfd, buf + pos, msg_size, 0);if (one_size == 0) {printf("client disconnect\n");return false;}if (one_size < 0) {perror("recv wrong\n");return false;}pos += one_size;msg_size -= one_size;}printf("message size = %d, message: %s\n", pos, buf);return true;
}

客户端调用send()函数时,由于可能当前缓冲区剩余空间不足以放下所有要发送的数据,因此也需要用send()函数的返回值来判断是否完成发送

// 替代 send() 函数
void send_message(int sockfd, const char* msg) {int msg_size = strlen(msg);int send_size = 0;send_size = send(sockfd, &msg_size, sizeof(int), 0);if (send_size < 0) {perror("send wrong\n");return;}int pos = 0;while (msg_size > 0) {send_size = send(sockfd, msg + pos, strlen(msg) - pos, 0);if (send_size < 0) {perror("send wrong\n");return;}pos += send_size;msg_size -= send_size;}printf("send message: %s\n", msg);
}

Linux socket编程(一):客户端服务端通信、解决TCP粘包相关推荐

  1. Netty解决TCP粘包/拆包导致的半包读写问题

    一.TCP粘包/拆包问题说明 TCP是个"流"协议,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包拆分,所以在业务上认为,一 ...

  2. golang解决TCP粘包问题

    6行代码解决golang TCP粘包 转自:https://studygolang.com/articles/12483 什么是TCP粘包问题以及为什么会产生TCP粘包,本文不加讨论.本文使用gola ...

  3. netty解决TCP粘包/拆包导致的半包读写问题的三种方案

    解决方案一:LineBasedFrameDecoder+StringDecoder来解决TCP的粘包/拆包问题 只需要在客户端和服务端加上45.46两行代码并且在发送消息的时候加上换行符即可解决TCP ...

  4. c#解决TCP“粘包”问题

    c#解决TCP"粘包"问题 参考文章: (1)c#解决TCP"粘包"问题 (2)https://www.cnblogs.com/wangjun8868/p/71 ...

  5. 《精通并发与Netty》学习笔记(13 - 解决TCP粘包拆包(一)概念及实例演示)

    一.粘包/拆包概念 TCP是一个"流"协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的 ...

  6. golang 解决 TCP 粘包问题

    什么是 TCP 粘包问题以及为什么会产生 TCP 粘包,本文不加讨论.本文使用 golang 的 bufio.Scanner 来实现自定义协议解包. 协议数据包定义 本文模拟一个日志服务器,该服务器接 ...

  7. swoole 解决tcp粘包问题

    Tcp粘包问题 tcp在发送数据的时候因为存在数据缓存的关系,对于数据在发送的时候在 短时间内 如果连续发送很多小的数据的时候就会有可能一次性一起发送,还有就是对于大的数据就会分开连续发送多次 Tcp ...

  8. Golang解决TCP粘包拆包问题

    协议定义 报文长度(4字节) 报文内容[]byte 服务端代码 package mainimport ("encoding/binary""fmt"" ...

  9. TCP聊天文件服务器v2.2 - 服务端客户端套接字解决分包/粘包问题 - SocketQueue继承以及减少冗余

    TCP聊天+传输文件服务器服务器套接字v2.2 整个图当封面吧 所有版本记录: v1.0 : TCP聊天服务器套接字|PyQt5+socket(TCP端口映射+端口放行)+logging+Thread ...

最新文章

  1. IT从业人员必看的10大论坛(ZT)
  2. 介绍一下Objective-c常用的函数,常数变量
  3. python integer_【Python】string/list/integer常用函数总结
  4. 主程序与子程序不在同一程序模块中_数控车床子程序M98、M99编程实例!
  5. 拍照尺寸 ios_iOS 14照片和相机:QuickTake快捷键,照片标题,镜像自拍照等
  6. python在信号处理的应用_Python和信号处理程序
  7. 信息的哲学--从信息到数据存储,再到数据保护
  8. 如何让程序像人一样的去批量下载歌曲?Python爬取付费歌曲
  9. (java毕业设计)基于java汽车租赁管理系统源码
  10. 杜凯杰教学数据分析:python 图片爬取 爬取各校校花图片
  11. JS Worker执行多线程
  12. 《UnityAPI.ParticleSystem粒子系统》(Yanlz+Unity+SteamVR+云技术+5G+AI+VR云游戏+Particle+loop+Emit+立钻哥哥++OK++)
  13. 开源OCR文字识别软件Calamari
  14. Foundry 中文文档发布啦
  15. Windows注册服务的两种方式,并设置服务开机自启
  16. 《SolidWorks 2014中文版机械设计从入门到精通》——1.4 操作环境设置
  17. 智能安防的基本介绍,智能安防主要有哪几部分构成?
  18. Beautifulsoup官方文档
  19. Node.js 微服务实践:基于容器的一站式命令行工具链
  20. 工作室机房干扰问题怎样解决?

热门文章

  1. python 自动生成问卷表的软件的设计与实现 毕业设计源码291138
  2. mysql复购率_MySQL_复购回购率
  3. 2022内蒙古最新建筑施工塔式起重机(建筑特种作业)模拟考试题库及答案
  4. web前端程序员到底值多少钱?
  5. 超全!全国近90所大学考研报录比汇总!
  6. 中国人离婚率高达76.4%?这些指标背后真实的状况到底是什么样的?
  7. Python异步并发机制详解,让你的代码运行效率就像搭上了火箭!!!
  8. 第八周--项目1--实现复数类中的运算符重载
  9. python计算球体体积_如何在Python中用MonteCarloMethod计算10维球体的体积?
  10. 头条号权重高有什么优势?头条权重在线查询