Tcp是一个面向连接的流数据传输协议,用人话说就是传输是一个已经建立好连接的管道,数据都在管道里像流水一样流淌到对端。那么数据必然存在几个问题,比如数据如何持续的读取,数据包的边界等。

Nagle's算法

Nagle 算法的核心思想是,在一个 TCP 连接上,最多只能有一个未被确认的小数据包(小于 MSS,即最大报文段大小)

优势

减少网络拥塞:通过合并小数据包,减少了网络中的数据包数量,降低了拥塞的可能性。

提高网络效率:在低速网络中,Nagle 算法可以显著提高传输效率。

劣势

增加延迟:在交互式应用中,Nagle 算法可能导致显著的延迟,因为它等待 ACK 或合并数据包。

C#中如何配置?

 var _socket = new Socket(IPAddress.Any.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_serverSocket.NoDelay = _options.NoDelay;

连接超时

在调用客户端Socket连接服务器的时候,可以设置连接超时机制,具体可以传入一个任务的取消令牌,并且设置超时时间。

CancellationTokenSource connectTokenSource = new CancellationTokenSource();
connectTokenSource.CancelAfter(3000); //3秒
await _socket.ConnectAsync(RemoteEndPoint, connectTokenSource.Token);

SSL加密传输

TCP使用SSL加密传输,通过非对称加密的方式,利用证书,保证双方使用了安全的密钥加密了报文。

在C#中如何配置?

服务端配置
//创建证书对象
var _certificate = _certificate = new X509Certificate2(_options.PfxCertFilename, _options.PfxPassword); //与客户端进行验证
if (allowingUntrustedSSLCertificate) //是否允许不受信任的证书
{
SslStream = new SslStream(NetworkStream, false,
(obj, certificate, chain, error) => true);
}
else
{
SslStream = new SslStream(NetworkStream, false);
} try
{
//serverCertificate:用于对服务器进行身份验证的 X509Certificate
//clientCertificateRequired:一个 Boolean 值,指定客户端是否必须为身份验证提供证书
//checkCertificateRevocation:一个 Boolean 值,指定在身份验证过程中是否检查证书吊销列表
await SslStream.AuthenticateAsServerAsync(new SslServerAuthenticationOptions()
{
ServerCertificate = x509Certificate,
ClientCertificateRequired = mutuallyAuthenticate,
CertificateRevocationCheckMode = checkCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck
}, cancellationToken).ConfigureAwait(false); if (!SslStream.IsEncrypted || !SslStream.IsAuthenticated)
{
return false;
} if (mutuallyAuthenticate && !SslStream.IsMutuallyAuthenticated)
{
return false;
}
}
catch (Exception)
{
throw;
} //完成验证后,通过SslStream传输数据
int readCount = await SslStream.ReadAsync(buffer, _lifecycleTokenSource.Token)
.ConfigureAwait(false);
客户端配置
var _certificate = new X509Certificate2(_options.PfxCertFilename, _options.PfxPassword);

if (_options.IsSsl) //如果使用ssl加密传输
{
if (_options.AllowingUntrustedSSLCertificate)//是否允许不受信任的证书
{
_sslStream = new SslStream(_networkStream, false,
(obj, certificate, chain, error) => true);
}
else
{
_sslStream = new SslStream(_networkStream, false);
} _sslStream.ReadTimeout = _options.ReadTimeout;
_sslStream.WriteTimeout = _options.WriteTimeout;
await _sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
{
TargetHost = RemoteEndPoint.Address.ToString(),
EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12,
CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
ClientCertificates = new X509CertificateCollection() { _certificate }
}, connectTokenSource.Token).ConfigureAwait(false); if (!_sslStream.IsEncrypted || !_sslStream.IsAuthenticated ||
(_options.MutuallyAuthenticate && !_sslStream.IsMutuallyAuthenticated))
{
throw new InvalidOperationException("SSL authenticated faild!");
}
}

KeepAlive

keepAlive不是TCP协议中的,而是各个操作系统本身实现的功能,主要是防止一些Socket突然断开后没有被感知到,导致一直浪费资源的情况。

其基本原理是在此机制开启时,当长连接无数据交互一定时间间隔时,连接的一方会向对方发送保活探测包,如连接仍正常,对方将对此确认回应

C#中如何调用操作系统的KeepAlive?

/// <summary>
/// 开启Socket的KeepAlive
/// 设置tcp协议的一些KeepAlive参数
/// </summary>
/// <param name="socket"></param>
/// <param name="tcpKeepAliveInterval">没有接收到对方确认,继续发送KeepAlive的发送频率</param>
/// <param name="tcpKeepAliveTime">KeepAlive的空闲时长,或者说每次正常发送心跳的周期</param>
/// <param name="tcpKeepAliveRetryCount">KeepAlive之后设置最大允许发送保活探测包的次数,到达此次数后直接放弃尝试,并关闭连接</param>
internal static void SetKeepAlive(this Socket socket, int tcpKeepAliveInterval, int tcpKeepAliveTime, int tcpKeepAliveRetryCount)
{
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, tcpKeepAliveInterval);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, tcpKeepAliveTime);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, tcpKeepAliveRetryCount);
}

具体的开启,还需要看操作系统的版本以及不同操作系统的支持。

粘包断包处理

Pipe & ReadOnlySequence



上图来自微软官方博客:https://devblogs.microsoft.com/dotnet/system-io-pipelines-high-performance-io-in-net/

TCP面向应用是流式数据传输,所以接收端接到的数据是像流水一样从管道中传来,每次取到的数据取决于应用设置的缓冲区大小,以及套接字本身缓冲区待读取字节数

C#中提供的Pipe就如上图一样,是一个管道

Pipe有两个对象成员,一个是PipeWriter,一个是PipeReader,可以理解为一个是生产者,专门往管道里灌输数据流,即字节流,一个是消费者,专门从管道里获取字节流进行处理。

可以看到Pipe中的数据包是用链表关联的,但是这个数据包是从Socke缓冲区每次取到的数据包,它不一定是一个完整的数据包,所以这些数据包连接起来后形成了一个C#提供的另外一个抽象的对象ReadOnlySequence

但是这里还是没有提供太好的处理断包和粘包的办法,因为断包粘包的处理需要两方面

1.业务数据包的定义

2.数据流切割出一个个完整的数据包

假设业务已经定义好了数据包,那么我们如何从Pipe中这些数据包根据业务定义来从不同的数据包中切割出一个完整的包,那么就需要ReadOnlySequence,它提供的操作方法,非常方便我们去切割数据,主要是头尾数据包的切割。

假设我们业务层定义了一个数据包结构,数据包是不定长的,包体长度每次都写在包头里,我们来实现一个数据包过滤器。

//收到消息
while (!_receiveDataTokenSource.Token.IsCancellationRequested)
{
try
{
//从pipe中获取缓冲区
Memory<byte> buffer = _pipeWriter.GetMemory(_options.BufferSize);
int readCount = 0;
readCount = await _sslStream.ReadAsync(buffer, _lifecycleTokenSource.Token).ConfigureAwait(false); if (readCount > 0)
{ var data = buffer.Slice(0, readCount);
//告知消费者,往Pipe的管道中写入了多少字节数据
_pipeWriter.Advance(readCount);
}
else
{
if (IsDisconnect())
{
await DisConnectAsync();
} throw new SocketException();
} FlushResult result = await _pipeWriter.FlushAsync().ConfigureAwait(false);
if (result.IsCompleted)
{
break;
}
}
catch (IOException)
{
//TODO log
break;
}
catch (SocketException)
{
//TODO log
break;
}
catch (TaskCanceledException)
{
//TODO log
break;
}
} _pipeWriter.Complete();
//消费者处理数据
while (!_lifecycleTokenSource.Token.IsCancellationRequested)
{
ReadResult result = await _pipeReader.ReadAsync();
ReadOnlySequence<byte> buffer = result.Buffer;
ReadOnlySequence<byte> data;
do
{
//通过过滤器得到一个完整的包
data = _receivePackageFilter.ResolvePackage(ref buffer); if (!data.IsEmpty)
{
OnReceivedData?.Invoke(this, new ClientDataReceiveEventArgs(data.ToArray()));
}
}
while (!data.IsEmpty && buffer.Length > 0);
_pipeReader.AdvanceTo(buffer.Start);
} _pipeReader.Complete();
/// <summary>
/// 解析数据包
/// 固定报文头解析协议
/// </summary>
/// <param name="headerSize">数据报文头的大小</param>
/// <param name="bodyLengthIndex">数据包大小在报文头中的位置</param>
/// <param name="bodyLengthBytes">数据包大小在报文头中的长度</param>
/// <param name="IsLittleEndian">数据报文大小端。windows中通常是小端,unix通常是大端模式</param>
/// </summary>
/// <param name="sequence">一个完整的业务数据包</param>
public override ReadOnlySequence<byte> ResolvePackage(ref ReadOnlySequence<byte> sequence)
{
var len = sequence.Length;
if (len < _bodyLengthIndex) return default;
var bodyLengthSequence = sequence.Slice(_bodyLengthIndex, _bodyLengthBytes);
byte[] bodyLengthBytes = ArrayPool<byte>.Shared.Rent(_bodyLengthBytes);
try
{
int index = 0;
foreach (var item in bodyLengthSequence)
{
Array.Copy(item.ToArray(), 0, bodyLengthBytes, index, item.Length);
index += item.Length;
} long bodyLength = 0;
int offset = 0;
if (!_isLittleEndian)
{
offset = bodyLengthBytes.Length - 1;
foreach (var bytes in bodyLengthBytes)
{
bodyLength += bytes << (offset * 8);
offset--;
}
}
else
{ foreach (var bytes in bodyLengthBytes)
{
bodyLength += bytes << (offset * 8);
offset++;
}
} if (sequence.Length < _headerSize + bodyLength)
return default; var endPosition = sequence.GetPosition(_headerSize + bodyLength);
var data = sequence.Slice(0, endPosition);//得到完整数据包
sequence = sequence.Slice(endPosition);//缓冲区中去除取到的完整包 return data;
}
finally
{
ArrayPool<byte>.Shared.Return(bodyLengthBytes);
}
}

以上就是实现了固定数据包头实现粘包断包处理的部分代码。

关于TCP的连接还有一些,比如客户端连接限制,空闲连接关闭等。如果大家对于完整代码感兴趣,可以看我刚写的一个TCP库:EasyTcp4Net:https://github.com/BruceQiu1996/EasyTcp4Net

C# 优雅的处理TCP数据(心跳,超时,粘包断包,SSL加密 ,数据处理等)的更多相关文章

  1. Socket/TCP粘包、多包和少包, 断包

    转发: https://blog.csdn.net/pi9nc/article/details/17165171 为什么TCP 会粘包 前几天,调试mina的TCP通信, 第一个协议包解析正常,第二个 ...

  2. UNIX网络编程——Socket/TCP粘包、多包和少包, 断包

    为什么TCP 会粘包 前几天,调试mina的TCP通信, 第一个协议包解析正常,第二个数据包不完整.为什么会这样吗,我们用mina这样通信框架,还会出现这种问题? TCP(transport cont ...

  3. 网络TCp数据的传输设计(黏包处理)

    //1.该片为引用别人的文章:http://www.cnblogs.com/alon/archive/2009/04/16/1437599.html 解决TCP网络传输"粘包"问题 ...

  4. 为什么TCP 会粘包断包UDP不会

    TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务.收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发 ...

  5. Tcp之心跳包

    Tcp之心跳包 心跳包 跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着. 事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很 ...

  6. 一文读懂Android进程及TCP动态心跳保活

    一直以来,APP进程保活都是 各软件提供商 和 个人开发者 头疼的问题.毕竟一切的商业模式都建立在用户对APP的使用上,因此保证APP进程的唤醒,提升用户的使用时间,便是软件提供商和个人开发者的永恒追 ...

  7. TCP之心跳包实现思路

    说起网络应用编程,想到最多的就是聊天类的软件.当然,在这类软件中,一般都会有一个用户掉线检测功能.今天我们就通过使用自定义的HeartBeat方式来检测用户的掉线情况. 心跳包实现思路 我们采用的思路 ...

  8. TCP数据包结构

    源端口号( 16 位):它(连同源主机 IP 地址)标识源主机的一个应用进程.目的端口号( 16 位):它(连同目的主机 IP 地址)标识目的主机的一个应用进程.这两个值加上 IP 报头中的源主机 I ...

  9. TCP/IP 笔记 - 超时和重传

    TCP协议为了提供可靠的数据传输服务,会启动数据重传来解决下层网络层(IP)可能出现的数据包丢失. 超时重传介绍 TCP重传由两套独立机制来完成重传,基于时间的超时重传(RTO,TCP发送数据时会设置 ...

  10. 【转】使用TCP协议连续传输大量数据时,是否会丢包,应如何避免?

    使用TCP协议连续传输大量数据时,是否会丢包,应如何避免? 比如发送文件.记得有人提过可能会发生什么堆栈溢出.怎样避免呢?是不是可以收到数据后发送确认包,收到确认包后再继续发送.或是发送方发送了一些数 ...

随机推荐

  1. CF1842

    A 比两边和的大小即可. B 显然如果一个数拥有的所有二进制位的 \(1\) 被包含在 \(x\) 中,选了一定不会导致不能变成 \(x\):如果有一个 \(1\),\(x\) 对应的位上是 \(0\ ...

  2. JS leetcode 两个数组的交集I II 合集题解分析

    壹 ❀ 引 前些日子,在与博客园用户MrSmileZhu闲聊中,我问到了他先前在字节跳动面试中遇到了哪些算法题(又戳到了他的伤心处),因为当时面试的高度紧张,原题描述已经无法重现了,但大概与数组合并. ...

  3. C语言,结构体成员的地址

    先回顾一个基础的知识,不同类型的数据在16位,32位,64位的机器分别占用多少字节. 类型 16位机器(字节) 32位机器(字节) 64位机器(字节) char 1 1 1 short 2 2 2 i ...

  4. 【Unity3D】半球卷屏特效

    1 原理 ​ 凸镜贴图 和 渐变凸镜贴图 中介绍了使用 OpenGL 实现凸镜贴图及其原理,通过顶点坐标映射到纹理坐标,并构造三角形网格,构建了真正的三维凸镜模型.本文通过 Shader 实现半球卷屏 ...

  5. wordpress设置固定链接404及伪静态配置

    说明 最近在将wordpress设置中文章url修改为月份和名称型 之后访问文章出现404.原因是配有配置好apache的伪静态. 配置步骤 1.修改httpd.conf 我这里是centos7,默认 ...

  6. vscode添加自定义html片段

    最近在学vue,用的是微软的vscode 开发工具. 很不错,赞一下微软.里面包含了众多插件大家可以各取所需. 另外有一项实用的功能,User Snippets 用户自定义代码段, 对于那些需要重复编 ...

  7. pycharm—flask创建简单web项目

    1.系统 系统 版本 OS win 10 pycharm 专业版2022.3.1 2.引入flask包 pip install flask 3.项目目录展示.代码.浏览器访问 from flask i ...

  8. Qt5.15.0 升级至 Qt5.15.9 遇到的一些错误

    按照之前我写的文章教程,可以很简单的编译出静态库(仅供学习交流) 编译 windows 上的 qt 静态库 编译出静态库后,替换旧版本的库,见我另一篇文章教程 VS2019 配置 QT 库 之所以没有 ...

  9. Apifox:成熟的测试工具要学会自己写接口文档

    好家伙, 在开发过程中,我们总是避免不了进行接口的测试, 而相比手动敲测试代码,使用测试工具进行测试更为便捷,高效 今天发现了一个非常好用的接口测试工具Apifox 相比于Postman,他还拥有一个 ...

  10. 固态硬盘使用f2fs作为根分区安装linux

    目录 前言 碰到的问题 对策 我的实际操作步骤 0.警告 1. 准备 2. 分区 3. 使用网络安装debian10 4. 备份根分区 5. 修改固态硬盘linux根分区为f2fs 6.恢复备份 7. ...