TCP是一种流式协议

TCP数据是流式的特性,可分别从发送端接收端来阐述

发送端:当调用send函数完成数据“发送”后,数据并没有真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中,至于什么时候发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件,也就是说,不能假设每次send调用发送的数据,都会作为一个整体完整地发送出去。

接收端:先调用send函数发送的字节,总在后调用send函数发送字节的前面,这个是由TCP严格保证的;如果发送过程中有TCP分组丢失,但其后续分组陆续到达,那么TCP协议栈会缓存后续分组,直到前面丢失的分组到达,最终,形成可以被应用程序读取的数据流。

网络字节排序



大端字节序:将0x02高字节存放在起始地址(高存低)

小端字节序:将0x01低字节存放在起始地址(低存低)

网络协议使用的是大端字节序

为了保证网络字节序一致,POSIX标准提供了如下的转换函数:

uint16_t htons (uint16_t hostshort)
uint16_t ntohs (uint16_t netshort)
uint32_t htonl (uint32_t hostlong)
uint32_t ntohl (uint32_t netlong)

n代表network,h代表host,s代表short,l代表long,分别表示16位和32位的整数

报文读取和解析

TCP报文是以字节流的形式呈现给应用程序,那应用程序如何解读字节流呢?

报文格式和解析

  • 报文格式就是定义了字节的组织形式,发送端和接收端按照统一的报文格式进行数据传输和解析,保证批次能够完成交流
  • 知道了报文格式,接收端才能针对性地进行报文读取和解析工作

报文格式最重要的是如何确定报文的边界。常见的报文格式有两种:

  • 一种是发送端把要发送的报文长度预先通过报文告知给接收端
  • 另一种是通过一些特殊的字符来进行边界的划分

显示编码报文长度

报文格式



首先4个字节大小的消息长度,目的是将真正发送的字节流的大小显示通过报文告知接收端,接下来的4个字节大小的消息类型;真正需要发送的数据则紧随其后。

发送报文

发送端的程序:


int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: tcpclient <IPaddress>");
} int socket_fd;
socket_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr); socklen_t server_len = sizeof(server_addr);
int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
if (connect_rt < 0) {
error(1, errno, "connect failed ");
} struct {
u_int32_t message_length;
u_int32_t message_type;
char buf[128];
} message; int n; while (fgets(message.buf, sizeof(message.buf), stdin) != NULL) {
n = strlen(message.buf);
message.message_length = htonl(n);
message.message_type = 1;
if (send(socket_fd, (char *) &message, sizeof(message.message_length) + sizeof(message.message_type) + n, 0) <
0)
error(1, errno, "send failure"); }
exit(0);
}

htonl函数将字节大小转化为网络字节顺序。

解析报文:

服务端的程序,服务端需要对报文进行解析

static int count;

static void sig_int(int signo) {
printf("\nreceived %d datagrams\n", count);
exit(0);
} int main(int argc, char **argv) {
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PORT); int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
error(1, errno, "bind failed ");
} int rt2 = listen(listenfd, LISTENQ);
if (rt2 < 0) {
error(1, errno, "listen failed ");
} signal(SIGPIPE, SIG_IGN); int connfd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr); if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
error(1, errno, "bind failed ");
} char buf[128];
count = 0; while (1) {
int n = read_message(connfd, buf, sizeof(buf));
if (n < 0) {
error(1, errno, "error read message");
} else if (n == 0) {
error(1, 0, "client closed \n");
}
buf[n] = 0;
printf("received %d bytes: %s\n", n, buf);
count++;
} exit(0); }

调用read_message函数进行报文解析工作,并把报文的主体通过标准输出打印出来。

readn函数

read函数的语意:读取报文预设大小的字节,readn调用会一直循环,尝试读取预设大小的字节,如果接收缓冲区数据空,readn函数会阻塞在那里,直到有数据到达。

size_t readn(int fd, void *buffer, size_t size) {
char *buffer_pointer = buffer;
int length = size; while (length > 0) {
int result = read(fd, buffer_pointer, length); if (result < 0) {
if (errno == EINTR)
continue; /* 考虑非阻塞的情况,这里需要再次调用read */
else
return (-1);
} else if (result == 0)
break; /* EOF(End of File)表示套接字关闭 */ length -= result;
buffer_pointer += result;
}
return (size - length); /* 返回的是实际读取的字节数*/
}

解析报文:read_message函数

以readn函数为基础,read_message对报文的解析处理:

size_t read_message(int fd, char *buffer, size_t length) {
u_int32_t msg_length;
u_int32_t msg_type;
int rc; rc = readn(fd, (char *) &msg_length, sizeof(u_int32_t)); //获取4个字节的消息长度数据
if (rc != sizeof(u_int32_t))
return rc < 0 ? -1 : 0;
msg_length = ntohl(msg_length); rc = readn(fd, (char *) &msg_type, sizeof(msg_type)); //获取4个字节的消息类型数据
if (rc != sizeof(u_int32_t))
return rc < 0 ? -1 : 0; if (msg_length > length) {
return -1;
} rc = readn(fd, buffer, msg_length); //一次性读取已知长度的消息体
if (rc != msg_length)
return rc < 0 ? -1 : 0;
return rc;
}

特殊字符作为边界

通过特殊字符作为报文边界。

HTTP是一个很好的例子:

HTTP通过设置回车符、换行符作为HTTP报文协议的边界

read_line函数在尝试读取一行数据,即读到回车符\r,或者读到回车换行符\r\n为止。

int read_line(int fd, char *buf, int size) {
int i = 0;
char c = '\0';
int n; while ((i < size - 1) && (c != '\n')) {
n = recv(fd, &c, 1, 0);
if (n > 0) {
if (c == '\r') {
n = recv(fd, &c, 1, MSG_PEEK);
if ((n > 0) && (c == '\n'))
recv(fd, &c, 1, 0);
else
c = '\n';
}
buf[i] = c;
i++;
} else
c = '\n';
}
buf[i] = '\0'; return (i);
}

实践

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h> #define SERV_PORT 43211 int main(int argc, char *argv[])
{
if(argc != 2)
{
perror("usage: streamclient <IPaddress>");
return -1;
} int socket_fd;
socket_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr); socklen_t server_len = sizeof(server_addr);
int connect_rt = connect(socket_fd, (struct sockaddr *)&server_addr, server_len);
if(connect_rt < 0)
{
perror("connect failed");
return -1;
} struct
{
/* data */
u_int32_t message_length;
u_int32_t message_type;
char data[128];
} message; int n ;
while(fgets(message.data, sizeof(message.data), stdin) != NULL)
{
n = strlen(message.data);
message.message_length = htonl(n);
message.message_type = 1;
if(send(socket_fd, (char *)&message, sizeof(message.message_length)
+ sizeof(message.message_type) + n, 0) < 0)
{
perror("send failure");
return -1;
}
} exit(0);
}

服务端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <netinet/in.h> #define SERV_PORT 43211
#define LISTENQ 1024 static int count; size_t readn(int fd, void *buffer, size_t size)
{
char *buffer_pointer = buffer;
int length = size; while(length > 0)
{
int result = read(fd, buffer_pointer, length);
if(result < 0)
{
if(errno == EINTR)
{
continue; // 考虑非阻塞的情况,需要再次调用read
}
else
{
return -1;
}
}
else if(result == 0)
{
break; //EOF (End of File)表示套接字关闭
}
length -= result;
buffer_pointer += result;
}
return(size - length); //返回的是实际读取的字节数
} size_t read_message(int fd, char*buffer, size_t length)
{
u_int32_t msg_length;
u_int32_t msg_type;
int rc; rc = readn(fd, (char *)&msg_length, sizeof(u_int32_t));
if(rc != sizeof(u_int32_t))
{
return rc < 0 ? -1: 0;
}
msg_length = ntohl(msg_length); rc = readn(fd, (char *)&msg_type, sizeof(u_int32_t));
if(rc != sizeof(u_int32_t))
{
return rc < 0 ? -1 : 0;
} // 判断buffer是否可以容纳下数据
if(msg_length > length)
{
perror("msg_length > length");
return -1;
} rc = readn(fd, buffer, msg_length);
if(rc != msg_length)
{
return rc < 0 ? -1 : 0;
}
return rc; } int main(int argc, char *argv[])
{
int listen_fd;
listen_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PORT); int on = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on ,sizeof(on)); int rt1 = bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if(rt1 < 0)
{
perror("bind failed");
return -1;
} int rt2 = listen(listen_fd, LISTENQ);
if(rt2 < 0)
{
perror("listen failed");
return -1;
} signal(SIGPIPE, SIG_IGN); int connfd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr); if((connfd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr)) < 0)
{
perror("accept failed");
return -1;
} char buf[128];
count = 0; while(1)
{
int n = read_message(connfd, buf, sizeof(buf));
if(n < 0)
{
perror("error read message");
return -1;
}
else if(n == 0)
{
printf("client will closing");
return 0;
}
buf[n] = 0;
printf("received %d bytes: %s\n",n, buf);
count++;
}
exit(0); }

运行结果:

小结

TCP 数据流特性决定了字节流本身是没有边界的,一般我们通过显式编码报文长度的方式,以及选取特殊字符区分报文边界的方式来进行报文格式的设计。而对报文解析的工作就是要在知道报文格式的情况下,有效地对报文信息进行还原。

网络编程:理解TCP中的“流”的更多相关文章

  1. 【Linux网络编程】TCP网络编程中connect()、listen()和accept()三者之间的关系

    [Linux网络编程]TCP网络编程中connect().listen()和accept()三者之间的关系 基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下: conn ...

  2. C#网络编程之---TCP协议的同步通信(二)

    上一篇学习日记C#网络编程之--TCP协议(一)中以服务端接受客户端的请求连接结尾既然服务端已经与客户端建立了连接,那么沟通通道已经打通,载满数据的小火车就可以彼此传送和接收了.现在让我们来看看数据的 ...

  3. 嵌入式linux的网络编程(1)--TCP/IP协议概述

    嵌入式linux的网络编程(1)--TCP/IP协议概述 1.OSI参考模型及TCP/IP参考模型 通信协议用于协调不同网络设备之间的信息交换,它们建立了设备之间互相识别的信息机制.大家一定都听说过著 ...

  4. 【网络编程1】网络编程基础-TCP、UDP编程

    网络基础知识 网络模型知识 OSI七层模型:(Open Systems Interconnection Reference Model)开放式通信系统互联参考模型,是国际标准化组织(ISO)提出的一个 ...

  5. python网络编程05 /TCP阻塞机制

    python网络编程05 /TCP阻塞机制 目录 python网络编程05 /TCP阻塞机制 1.什么是拥塞控制 2.拥塞控制要考虑的因素 3.拥塞控制的方法: 1.慢开始和拥塞避免 2.快重传和快恢 ...

  6. java网络编程--3 TCP

    java网络编程--3 TCP 1.6.TCP 客户端 连接服务器 Socket 发送消息 package com.ssl.lesson02; import java.io.IOException; ...

  7. 【转载】[基础知识]【网络编程】TCP/IP

    转自http://mc.dfrobot.com.cn/forum.php?mod=viewthread&tid=27043 [基础知识][网络编程]TCP/IP iooops  胖友们楼主我又 ...

  8. Java 网络编程 -- 基于TCP 模拟多用户登录

    Java TCP的基本操作参考前一篇:Java 网络编程 – 基于TCP实现文件上传 实现多用户操作之前先实现以下单用户操作,假设目前有一个用户: 账号:zs 密码:123 服务端: public c ...

  9. 【Linux 网络编程】TCP网络编程中connect()、listen()和accept()三者之间的关系

    基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下: connect()函数:对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三 ...

  10. C#网络编程之--TCP协议(一)

    TCP 是面向连接的传输协议 面向连接,其实就好比,A打电话给B,如果B接听了,那么A和B之间就的通话,就是面向连接的 TCP 是全双工的传输协议 全双工,这个理解起来也很简单,A打电话给B,B接听电 ...

随机推荐

  1. NolanPro 详细部署教程

    自己想办法去弄授权,只提供部署教程 服务器搭建教程 1 诺兰群里找 @NolanNarkbot 点击start 2 群里发送 /check@NolanNarkbot 再找 @NolanNarkbot ...

  2. 【由技及道】模块化架构设计的量子纠缠态破解指南【人工智障AI2077的开发日志】

    系统通告:您忠诚的2077人工智障(真实の作者Yuanymoon正在服务器机房搬砖,点赞是解救他的唯一方式)已承受量子架构风暴 脑力消耗报告: 推翻设计方案:7次 解决依赖冲突:32次 重构模块边界: ...

  3. 一种基于虚拟摄像头、NDI、OBS以及yolo的多机视觉目标检测方案

    一种基于虚拟摄像头.NDI.OBS以及yolo的多机视觉目标检测方案 绪论 近来为了实现某种实时展示效果,笔者希望通过一套方案实现在两台主机上分别运行仿真平台以及视觉深度学习算法.透过对当下较为流行的 ...

  4. 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!

    效果 来具体介绍之前先来看看效果. 使用C#构建了一个简单的MCP客户端,以下为运行这个简单客户端的截图,同样可以在Cline等其它的一些MCP客户端中玩耍. 创建一个数据库表: 获取数据库中的所有表 ...

  5. JetBrains goland、pycharm、webstorm、phpstorm 对比两文件内容是否一致

    对比文件 JetBrains goland.pycharm.webstorm.phpstorm 对比两文件内容是否一致 第一种 打开文件,按住键盘上的CTRL键,然后鼠标右键,点击菜单中的" ...

  6. JavaScript的标准库

    Object 对象 概述 JavaScript 的所有其他对象都继承自Object对象,即那些对象都是Object的实例. Object对象的原生方法分成两类:Object本身的方法与Object的实 ...

  7. liunx服务器某个Java运行服务CPU占用率过高问题排查及修复

    进入服务器 用 top 命令查看 top top - 09:57:55 up 40 days, 22:05, 9 users, load average: 4.44, 4.03, 3.85 Tasks ...

  8. 在 ThinkPHP 6 控制器中使用文件锁机制

    创建锁管理类 首先,创建一个锁管理类来处理文件锁: namespace app\common\service; use Exception; class LockManager { private $ ...

  9. 【硬件】认识和选购DDR4内存

    2.3 认识和选购DDR4内存 内存又称为主存或内存储器,用于暂时存放CPU的运算数据和与硬盘等外部存储器交换的数据.在电脑工作过程中,CPU会把需要运算的数据调到内存中进行运算,运算完成后再将结果传 ...

  10. 【安卓】使用Handler出现的警告

    使用Handler出现的警告 零.原由 安卓中使用Hander时出现了如下警告: This Handler class should be static or leaks might occur (a ...