首页 > 代码库 > 项目笔记---C#异步Socket示例

项目笔记---C#异步Socket示例

概要

在C#领域或者说.net通信领域中有着众多的解决方案,WCF,HttpRequest,WebAPI,Remoting,socket等技术。这些技术都有着自己擅长的领域,或者被合并或者仍然应用于某些场合。本文主要介绍Socket通讯,因其有着跨平台、跨语言、高性能等优势,适合某些情况的应用以及性能优越的解决方案。

本文是基于一个小项目中的应用,使用了异步方式的Socket通讯,性能上达到多客户端多点通讯,大文件(M-G级别)的文件传输,异步长连接上的性能优势,但此项目也有些不足:未进行大量的外网长时间传输测试,对不稳定的网络状况未做很好的处理,例如加入断点续传功能,对Socket传输错误(其中微软提供了大量的Socket通讯错误代码指示错误类型,请参考http://www.cnblogs.com/Aricc/archive/2010/01/29/1659134.html)未做一个很好的分类处理,只简单的统一为一个类型的错误。所以,此项目介绍只是想抛砖引玉介绍异步Socket通讯,如果有不足或改进之处,还请各位不吝指出。

同步和异步区别

这里的同步和异步指的是服务端Accept接受客户端连接请求的方式。在同步模式下,服务端要开启一个Thread线程循环监听可能来自客户端的服务,如果没有则阻塞,如果有连接则接受连接并存入Connection Pool连接池,这样一个连接(或者说一个连接线程,一般占用系统内存约为2M,具体原因请参考《CLR via C#》中线程章节),这样32位系统下单一应用程序最大内存不超过2G,也就是说,单一连接服务端所能接受客户端最大的请求数为1000(实测下为700+);而异步连接则应用非阻塞式异步连接机制(http://www.cnblogs.com/2018/archive/2011/05/10/2040333.html)BeginAccept异步接受客户端请求并执行相应请求逻辑,在执行完毕后系统能自动优化,并当再次应用时唤醒,从而达到可接受大量的客户端请求,但也限于“同时”执行的客户端数量,对于某些长连接客户端巨大,但并发性小的情景适用。

自定义协议

众所周知,在Socket通讯中传输的普通的字符串或者二进制数据流,不适用于一些复杂的情况,例如约定一个可扩展的协议,可变长协议等,所以本项目采用自定义协议类型来满足可扩展需求:

Header

协议头

命令

流1长度

流2长度

2字节

4字节

4字节

4字节

Body

流1

流2

N字节

N字节

说明:

协议头为:FF 7E ,2字节固定值

命令为:命令编号,如1001

流1长度:Int32型指示Body中的流1的长度

流2长度:Int32型指示Body中的流2的长度

流1:字节流

流2:字节流

 

这样,基于上述协议可自定义流1、流2的长度分别存放不同数据,基于协议还可以对数据协议进行封装,提供公共的解析方式。

  1     /// <summary>  2     /// 通讯二进制协议,此协议基于变长的流传输。  3     /// 注:扩展此方法成员时,请重写相关方法。  4     /// </summary>  5     /// <remarks>  6     /// Create By CYS  7     /// </remarks>  8     public class CommunicateProtocol : IDisposable  9     { 10         #region Public Properties 11         /// <summary> 12         /// Byte array length of flag 13         /// </summary> 14         public const int ByteLength_HeaderFlag = 2; 15         /// <summary> 16         /// Byte array length of command 17         /// </summary> 18         public const int ByteLength_HeaderCmd = 4; 19         /// <summary> 20         /// Byte array length of header stream1 21         /// </summary> 22         public const int ByteLength_HeaderStream1Len = 4; 23         /// <summary> 24         /// Byte array length of header stream2 25         /// </summary> 26         public const int ByteLength_HeaderStream2Len = 4; 27         /// <summary> 28         /// 协议头长度 29         /// </summary> 30         public static int FlagLen 31         { 32             get { return ByteLength_HeaderFlag; } 33         } 34         /// <summary> 35         /// 命令(Int32) 36         /// </summary> 37         public int Command 38         { 39             get 40             { 41                 return BitConverter.ToInt32(header_Cmd, 0); 42             } 43             set 44             { 45                 BitConverter.GetBytes(value).CopyTo(header_Cmd, 0); 46             } 47         } 48         /// <summary> 49         /// 流1长度 50         /// </summary> 51         /// <returns></returns> 52         public int Stream1Len 53         { 54             get 55             { 56                 return BitConverter.ToInt32(header_Stream1Len, 0); 57             } 58             set 59             { 60                 BitConverter.GetBytes(value).CopyTo(header_Stream1Len, 0); 61             } 62         } 63         /// <summary> 64         /// 流2长度 65         /// </summary> 66         /// <returns></returns> 67         public int Stream2Len 68         { 69             get 70             { 71                 return BitConverter.ToInt32(header_Stream2Len, 0); 72             } 73             set 74             { 75                 BitConverter.GetBytes(value).CopyTo(header_Stream2Len, 0); 76             } 77         } 78         #endregion Public Properties 79  80         #region Private Properties 81         private static byte[] header_Flag = new byte[ByteLength_HeaderFlag]; 82         private byte[] header_Cmd = new byte[ByteLength_HeaderCmd]; 83         private byte[] header_Stream1Len = new byte[ByteLength_HeaderStream1Len]; 84         private byte[] header_Stream2Len = new byte[ByteLength_HeaderStream1Len]; 85         private byte[] body_Stream1 = new byte[0]; 86         private Stream body_Stream2; 87         #endregion Private Properties 88  89         #region Constructor 90         /// <summary> 91         /// Static constructor 92         /// </summary> 93         static CommunicateProtocol() 94         { 95             header_Flag = new byte[ByteLength_HeaderFlag] { 0xFF, 0x7E }; 96         } 97  98         #endregion Constructor 99 100         #region Public Method101         /// <summary>102         /// 判断是否是协议头标志103         /// </summary>104         /// <param name="bytes"></param>105         /// <returns></returns>106         public static bool CheckFlag(byte[] bytes)107         {108             if (bytes.Length != header_Flag.Length)109                 return false;110             if (bytes.Length != 2)111                 return false;112             if (!bytes[0].Equals(header_Flag[0]) || !bytes[1].Equals(header_Flag[1]))113                 return false;114             return true;115         }116         /// <summary>117         /// SetStream1118         /// </summary>119         /// <param name="sm"></param>120         public void SetStream1(byte[] sm)121         {122             body_Stream1 = sm;123         }124         /// <summary>125         /// GetStream1126         /// </summary>127         /// <returns></returns>128         public byte[] GetStream1()129         {130             return body_Stream1;131         }132         /// <summary>133         /// SetStream2134         /// </summary>135         /// <param name="sm"></param>136         public void SetStream2(Stream sm)137         {138             body_Stream2 = sm;139         }140         /// <summary>141         /// body_Stream2142         /// </summary>143         /// <returns></returns>144         public Stream GetStream2()145         {146             return body_Stream2;147         }148         /// <summary>149         /// GetHeaderBytes150         /// </summary>151         /// <returns></returns>152         public byte[] GetHeaderBytes()153         {154             int offset = 0;155             byte[] bytes = new byte[ByteLength_HeaderFlag + ByteLength_HeaderCmd + ByteLength_HeaderStream1Len + ByteLength_HeaderStream2Len];156 157             Array.Copy(header_Flag, 0, bytes, 0, ByteLength_HeaderFlag); offset += ByteLength_HeaderFlag;158             Array.Copy(header_Cmd, 0, bytes, offset, ByteLength_HeaderCmd); offset += ByteLength_HeaderCmd;159             Array.Copy(header_Stream1Len, 0, bytes, offset, ByteLength_HeaderStream1Len); offset += ByteLength_HeaderStream1Len;160             Array.Copy(header_Stream2Len, 0, bytes, offset, ByteLength_HeaderStream2Len); offset += ByteLength_HeaderStream2Len;161 162             return bytes;163         }164         /// <summary>165         /// InitProtocolHeader166         /// </summary>167         /// <returns></returns>168         public static CommunicateProtocol InitProtocolHeader(byte[] source)169         {170             if (source.Length < ByteLength_HeaderCmd + ByteLength_HeaderStream1Len + ByteLength_HeaderStream2Len)171             {172                 throw new Exception("byte length is illegal");173             }174 175             byte[] header_cmd = new byte[ByteLength_HeaderCmd];176             byte[] header_stream1len = new byte[ByteLength_HeaderStream1Len];177             byte[] header_stream2len = new byte[ByteLength_HeaderStream2Len];178             Array.Copy(source, 0, header_cmd, 0, ByteLength_HeaderCmd);179             Array.Copy(source, ByteLength_HeaderCmd, header_stream1len, 0, ByteLength_HeaderStream1Len);180             Array.Copy(source, ByteLength_HeaderCmd + ByteLength_HeaderStream1Len, header_stream2len, 0, ByteLength_HeaderStream2Len);181 182             return new CommunicateProtocol183             {184                 Command = BitConverter.ToInt32(header_cmd, 0),185                 Stream1Len = BitConverter.ToInt32(header_stream1len, 0),186                 Stream2Len = BitConverter.ToInt32(header_stream2len, 0),187             };188         }189         #endregion Public Method190 191         #region Private Method192 193         #endregion Private Method194 195         #region IDisposable 成员196         /// <summary>197         /// Dispose198         /// </summary>199         public void Dispose()200         {201             header_Cmd = null;202             header_Stream1Len = null;203             header_Stream2Len = null;204             body_Stream1 = null;205             body_Stream2 = null;206         }207 208         #endregion209     }
View Code

 

通讯机制

传统意义上的上传与下载请求就是,一端发起Request请求,另一端接受并答复请求内容,这样就完成了一次请求应答。然而,如果要实现更多的控制功能,就要在这“一去一回”上增加通信应答次数,类似于TCP的三次握手请求。

其中,当请求被拒绝时,应向请求端发送错误答复998,这样就可以在这个过程中建立一个完善的请求答复机制。

 1     public abstract class ContractAdapter 2     { 3         #region Public Method 4         /// <summary> 5         ///  6         /// </summary> 7         /// <param name="p"></param> 8         /// <param name="s"></param> 9         protected void ExecuteProtocolCommand(CommunicateProtocol p, SocketState s)10         {11             if (p == null) throw new ArgumentNullException("CommunicateProtocol is null");12             switch (p.Command)13             {14                 case 0: CommandWrapper(((ICommandFunc)new Command0()), p, s); break;15                 case 1: CommandWrapper(((ICommandFunc)new Command1()), p, s); break;16                 case 2: CommandWrapper(((ICommandFunc)new Command2()), p, s); break;17                 case 998: CommandWrapper(((ICommandFunc)new Command998()), p, s); break;18                 case 999: CommandWrapper(((ICommandFunc)new Command999()), p, s); break;19                 // 20                 case 1001: CommandWrapper(((ICommandFunc)new Command1001()), p, s); break;21                 case 1002: CommandWrapper(((ICommandFunc)new Command1002()), p, s); break;22                 //23                 case 2001: CommandWrapper(((ICommandFunc)new Command2001()), p, s); break;24                 case 2002: CommandWrapper(((ICommandFunc)new Command2002()), p, s); break;25                 //26                 case 3001: CommandWrapper(((ICommandFunc)new Command3001()), p, s); break;27 28                 default: throw new Exception("Protocol type does not exist.");29             }30         }31         /// <summary>32         /// 33         /// </summary>34         /// <param name="func"></param>35         /// <param name="p"></param>36         /// <param name="s"></param>37         protected abstract void CommandWrapper(ICommandFunc func, CommunicateProtocol p, SocketState s);38         #endregion Public Method39     }
View Code

以及在“命令”中封装,下一个命令。

 1     /// <summary> 2     ///  3     /// </summary> 4     public class Command1002 : ICommandFunc 5     { 6         #region ICommandFunc 成员 7         public CommandProfile profile { get; set; } 8  9         /// <summary>10         /// 11         /// </summary>12         /// <param name="protocol"></param>13         /// <param name="state"></param>14         /// <param name="sobj"></param>15         /// <returns></returns>16         public SocketState Execute(CommunicateProtocol protocol, SocketState state, IHSSocket sobj)17         {18             state.IsReceiveThreadAlive = false;19 20             // Check File21             if (!FileHelper.IsFileExist(profile.UpLoadPath + profile.UpLoadFName))22             {23                 var p = ProtocolMgmt.InitProtocolHeader(998, System.Text.Encoding.UTF8.GetBytes("服务端文件不存在"), null);24                 ProtocolMgmt.SendProtocol(state, p, sobj);25                 state.OutPutMsg = string.Format("Command 1002 :服务端文件不存在");26                 return state;27             }28             if (!FileHelper.CanRead(profile.UpLoadPath + profile.UpLoadFName))29             {30                 var p = ProtocolMgmt.InitProtocolHeader(998, System.Text.Encoding.UTF8.GetBytes("文件已被打开或占用,请稍后重试"), null);31                 ProtocolMgmt.SendProtocol(state, p, sobj);32                 state.OutPutMsg = string.Format("Command 1002 :文件已被打开或占用");33                 return state;34             }35 36             FileInfo fi = new FileInfo(profile.UpLoadPath + profile.UpLoadFName);37             using (FileStream fs = new FileStream(profile.UpLoadPath + profile.UpLoadFName, FileMode.Open))38             {39                 var p = ProtocolMgmt.InitProtocolHeader(2002, System.Text.Encoding.UTF8.GetBytes(fi.Name), fs);40                 ProtocolMgmt.SendProtocol(state, p, sobj);41                 state.OutPutMsg = string.Format("Command 1002 :发送文件 {0} 成功。", fi.FullName);42             }43 44             return state;45         }46 47 48         #endregion49     }
View Code

代码结构

 

 项目分为客户端和服务端,其中都依赖于BLL和Core核心类库,Core中封装的是协议头的解析方式、抽象/接口方法、Socket缓冲读取、枚举委托、异步调用基类等,BLL中主要封装的是协议命令,如Command1、Command2001等的具体实现方式。

Core:

BLL:

UI.Client:

 

UI.Server:

 

核心思想是将Command的业务需求整合进BLL层次中,而Socket基本的通讯方式等公用功能整合进Core,将UI层释放开,在UI层用Log4net等开源插件进行组合。

 

 

总结

基于文字限制,不能讲代码中的每个细节都将到,分析到,还请各位谅解,其中如有不妥之处还请不吝赐教。没有质疑,就没有进步;只有不断的思考才能更好的掌握知识。

最后将程序的源码奉上,希望对您有帮助。

 

源码下载

 

之前做项目时,项目参考的很多引用没有记住,希望以后有时间补上。

项目中的IP地址,需要根据您的本机IP进行配置,请在WinformServer和WinformClient的Setting文件中更改,同时,还要更改其默认的上传和下载文件,这里没有写成基于OpenFile的方式只是为了演示异步Socket通讯。

引用

Socket通信错误码:http://www.cnblogs.com/Aricc/archive/2010/01/29/1659134.html

异步通讯:http://www.cnblogs.com/2018/archive/2011/05/10/2040333.html

 

项目笔记---C#异步Socket示例