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. NC25045 [USACO 2007 Jan S]Balanced Lineup

    题目链接 题目 题目描述 For the daily milking, Farmer John's N cows (1 ≤ N ≤ 100,000) always line up in the sam ...

  2. 【Unity3D】表面着色器

    1 前言 ​ 固定管线着色器一.固定管线着色器二 中介绍了 ShaderLib 的基本用法,本文将接着讲解表面着色器(Surface Shader)的用法.固定管线着色器基于 ShaderLib 命令 ...

  3. VS2019 添加三方文件夹遇到的坑

    在开发新项目时需要用到一些三方 API,这些三方 API 没有生成 lib,所以我们在 VS 编译器中添加这些三方文件夹的头文件路径后 会出现 ERROR LNK2019 的错误提示,这些提示通常都是 ...

  4. QT - Day 6

    跟着视频教程创建了翻金币的项目,花了好几个晚上才学习完. 视频地址:最新QT从入门到实战 感谢视频的教学,真是受益匪浅. 后面的代码参考了老师的模板以及文档的抒写格式. 发布到随笔中的目的一方面为了完 ...

  5. Direct2D CreateBitmap的使用

    当需要设置位图的混合模式时,应该使用ID2D1DeviceContext而不是ID2D1RenderTarget. 代码如下: #define WIN32_LEAN_AND_MEAN #include ...

  6. Youpk 脱壳机脱壳原理分析

    Youpk 是一个针对整体加固和Dex抽取加固壳的脱壳机 主要是基于虚拟机的,也就是基于VA的脱壳机, 相对FART出来的更晚一些, 厂商针对少一些, 脱壳位置相对更底层一些,还提供了Dex修复的工具 ...

  7. Java 使用 itext 向PDF插入数据和图片

    Java 使用 itext 向PDF插入数据和图片 一.下载Adobe Acrobat DC 二.制作模板 1.准备一个word模板,并转换成PDF格式 2.使用Adobe Acrobat DC打开P ...

  8. MySQL单表能存储多少条数据?

    MySQL是中小型网站普遍使用的数据库之一,然而,很多人并不清楚MySQL到底能支持多大的数据量,甚至对它产生误解.MySQL单表的上限,主要与操作系统支持的最大文件大小有关.事实上MySQL能承受的 ...

  9. 【LeetCode剑指offer#05】回文链表的两种解法+删除链表中间节点(链表的基本操作)

    回文链表 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表.如果是,返回 true :否则,返回 false . 示例 1: 输入:head = [1,2,2,1] 输出:true 示 ...

  10. [Linux] ubuntu系统使用zfs记录

    关于zfs 之前从B站视频里面了解到了btrfs这个文件系统,说是能够实现cow,然后我第一时间觉得这是个极好的特性,结果后来才发现,使用的时候并不多,而且只能通过cp --reflink的方式使用, ...