单线程Socket服务器+多客户端SendPacketsAsync传输数据与文件的示例

lizhibin11 2012-07-10 12:54:06
在论坛回帖时我贴过不少采用IOCP模式进行SOCKET通信的代码,里面涉及的东西除了IOCP相关的之外,还包括多种线程防止同步问题的方式,重复使用对象、队列和缓冲区,自定义序列化和反序列化,有自定协议与无自定协议自适应等,是长时间不断完善并在继续完善的代码。对于初学者来说可能比较复杂,所以我想贴一个单线程服务器的小示例供初学者参考,比较简单,高手就不用看了,呵呵。
单线程服务器只能通过一种方式来做,就是Socket.Select,这种方式的性能也很好,服务端和客户端同在我的笔记本上,可以并发60000+连接,连接速度和IOCP差不多,接收处理速度比IOCP慢三倍左右。虽然是三倍,但因为IOCP在60000个连接时接收处理也是毫秒级的,所以这个三倍从绝对数字上并不算慢。
传输文件时可能分包是比较直觉的方法,但windows提供了TransmitPackets(.net中为SendPacketsAsync),这个的优势google一下可以了解,除了性能优势之外,这种方式传输整个文件夹是非常方便的。带来的问题就是如果不单独建立接收文件的套接字,在文件包没有自定协议的情况下如何与带有自定协议的正常数据进行区分,所以需要接收时有自适应的处理过程。在IOCP模式中处理这个比较复杂,但在单线程服务器中就是通过套接字的一些标识进行简单的判断(示例中仅有一个定义文件的数据包,数据类型和流程比较简单,所以该标识实际上就是DataSock类的FileStream是否为null)。
下面的示例如果要作大量并发的测试,请将用于接受连接的套接字单独Poll,这个示例是把它和建立了连接的套接字一起Select了。示例很简单,有很多在性能上需要优化的地方。

服务端
class Program
{
public static void Main(string[] arg)
{
IPAddress ip = IPAddress.Parse("192.168.0.103");
int port = 5555;
List<Socket> ReadSock = new List<Socket>();
List<Socket> ErrorSock = new List<Socket>();
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
ReadSock.Add(server);
Dictionary<Socket, SockData> ClientRecvFile = new Dictionary<Socket, SockData>();
server.Bind(new IPEndPoint(ip, port));
server.Listen(10);
while (true)
{
List<Socket> rs = new List<Socket>(ReadSock);
List<Socket> es = new List<Socket>(ErrorSock);
Socket.Select(rs, null, es, -1);
foreach (Socket sk in rs)
{
if (sk == server)
{
AcceptSock(ReadSock, ErrorSock, sk, ClientRecvFile);
continue;
}
if (sk.Available == 0)
{
CloseSock(ReadSock, ErrorSock, sk, ClientRecvFile);
continue;
}
ReceiveFile(sk, ClientRecvFile);
}
foreach (Socket sk in es)
CloseSock(ReadSock, ErrorSock, sk, ClientRecvFile);
}
}

static void AcceptSock(List<Socket> readsock, List<Socket> errorsock,
Socket server, Dictionary<Socket,SockData> clientfs)
{
Socket client = server.Accept();
readsock.Add(client);
errorsock.Add(client);
SockData sd = new SockData();
sd.DataRecvLen = 4;
sd.Buffer = new byte[4096];
clientfs.Add(client, sd);
Console.WriteLine("{0}建立连接", client.RemoteEndPoint);
}

static void ReceiveFile(Socket client, Dictionary<Socket,SockData> clientfs)
{
SockData sd = clientfs[client];
if (sd.SockFile == null)
{
ReceiveData(client, sd);
return;
}
int len = client.Receive(sd.Buffer);
sd.SockFile.Write(sd.Buffer, 0, len);
sd.FileRecvLen += len;
Console.SetCursorPosition(0, Console.CursorTop);
Console.Write("{0:P}", (double)sd.FileRecvLen / sd.FileLen);
if (sd.FileRecvLen < sd.FileLen)
return;
sd.Clear();
Console.WriteLine();
}

static void ReceiveData(Socket client, SockData sd)
{
int len2 = client.Receive(sd.Buffer, sd.OffSet, sd.DataRecvLen, SocketFlags.None);
if (len2 != sd.DataRecvLen)
{
sd.SetBuffer(sd.OffSet + len2, sd.DataRecvLen - len2);
return;
}
if (sd.OffSet < 4)
{
int datalen = BitConverter.ToInt32(sd.Buffer, 0);
if (datalen <= 4)
{
sd.SetBuffer(0, 4);
return;
}
sd.SetBuffer(4, datalen - 4);
return;
}
DoSomething(client, sd);
}

static void DoSomething(Socket client, SockData sd)
{
string filename = sd.SetFileInfo();
Console.WriteLine("开始接收文件{0}", filename);
}

static void CloseSock(List<Socket> readsock, List<Socket> errorsock,
Socket client, Dictionary<Socket, SockData> clientfs)
{
Console.WriteLine("{0}断开连接", client.RemoteEndPoint);
client.Shutdown(SocketShutdown.Both);
client.Close();
readsock.Remove(client);
errorsock.Remove(client);
clientfs[client].Dispose();
clientfs.Remove(client);
}
}

class SockData : IDisposable
{
public int OffSet { get; set; }
public int DataRecvLen { get; set; }
public int FileLen { get; set; }
public int FileRecvLen { get; set; }
public FileStream SockFile { get; set; }
public byte[] Buffer { get; set; }

public void SetBuffer(int offset, int len)
{
OffSet = offset;
DataRecvLen = len;
}

public string SetFileInfo()
{
FileLen = BitConverter.ToInt32(Buffer, 4);
int filenamelen = BitConverter.ToInt32(Buffer, 8);
string filename = Encoding.Unicode.GetString(Buffer, 12, filenamelen);
SockFile = new FileStream(filename, FileMode.Create, FileAccess.Write);
return filename;
}

public void Clear()
{
OffSet = 0;
DataRecvLen = 4;
FileLen = 0;
FileRecvLen = 0;
if (SockFile == null)
return;
SockFile.Close();
SockFile = null;
}

public void Dispose()
{
if (SockFile != null)
SockFile.Close();
GC.SuppressFinalize(this);
}

~SockData()
{
if (SockFile != null)
SockFile.Close();
}
}
...全文
406 6 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
6 条回复
切换为时间正序
请发表友善的回复…
发表回复
lizhibin11 2012-07-13
  • 打赏
  • 举报
回复
[Quote=引用 4 楼 的回复:]
楼主,我想问你一个问题
我看过一些同步和异步的文章,好像同步方式一次只能接受一个客户端的求情,你怎么会做到60000的并发数?想不明白~~
[/Quote]
代码写得不清楚?是不是因为没注释?不行我语音跟你说一下。
lizhibin11 2012-07-13
  • 打赏
  • 举报
回复
[Quote=引用 4 楼 的回复:]
楼主,我想问你一个问题
我看过一些同步和异步的文章,好像同步方式一次只能接受一个客户端的求情,你怎么会做到60000的并发数?想不明白~~
[/Quote]
在windows上编程模型有六种,在.net中一共四种,分别是select、同步多线程、异步、SocketAsyncEventArgs,后两种实质上都是iocp。在这四种模型中,select是单线程就可以连接多客户端的,因为它每次都会扫描所有套接字的可读写和错误情况,他和同步多线程是完全不同的模式。
  • 打赏
  • 举报
回复
楼主,我想问你一个问题
我看过一些同步和异步的文章,好像同步方式一次只能接受一个客户端的求情,你怎么会做到60000的并发数?想不明白~~
lizhibin11 2012-07-10
  • 打赏
  • 举报
回复

客户端
class Program
{
static IPAddress ip = IPAddress.Parse("192.168.0.103");
static int port = 5555;
static Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
[STAThread]
static void Main(string[] args)
{
SocketAsyncEventArgs sendarg = new SocketAsyncEventArgs();
sendarg.Completed += new EventHandler<SocketAsyncEventArgs>(sendarg_Completed);
Console.WriteLine("正在连接");
while (true)
{
try { client.Connect(ip, port); Console.WriteLine("连接完毕"); break; }
catch { Console.WriteLine("连接失败,正在重试"); Thread.Sleep(1000); }
}
ThreadPool.QueueUserWorkItem(Receive);
while (true)
{
Console.WriteLine("回车选择要传输的文件");
Console.ReadLine();
OpenFileDialog fd = new OpenFileDialog();
if (fd.ShowDialog() != DialogResult.OK)
continue;
string pathname = fd.FileName;
string filename = fd.SafeFileName;
SendFile(client, pathname, filename, sendarg);
}
}
static void SendFile(Socket client, string pathname, string filename, SocketAsyncEventArgs sendarg)
{
List<SendPacketsElement> list = new List<SendPacketsElement>();
byte[] header = new byte[12 + filename.Length * 2];
FileStream fs = new FileStream(pathname, FileMode.Open, FileAccess.Read);
int filelen = (int)fs.Length;
fs.Close();
GCHandle handle = GCHandle.Alloc(header);
IntPtr p = Marshal.UnsafeAddrOfPinnedArrayElement(header, 0);
Marshal.WriteInt32(p, header.Length);
Marshal.WriteInt32(p + 4, filelen);
Marshal.WriteInt32(p + 8, filename.Length * 2);
handle.Free();
Encoding.Unicode.GetBytes(filename, 0, filename.Length, header, 12);
list.Add(new SendPacketsElement(header));
list.Add(new SendPacketsElement(pathname));
sendarg.SendPacketsElements = list.ToArray();
client.SendPacketsAsync(sendarg);
}

static void sendarg_Completed(object sender, SocketAsyncEventArgs e)
{
Console.WriteLine("传输完毕");
}

static void Receive(object obj)
{
byte[] buffer = new byte[4096];
while (true)
{
if (!client.Connected)
{
Console.WriteLine("正在连接");
try { client.Connect(ip, port); Console.WriteLine("连接完毕"); }
catch { Console.WriteLine("连接失败,正在重试"); Thread.Sleep(1000); }
continue;
}
SocketError error;
int len = client.Receive(buffer, 0, buffer.Length, SocketFlags.None, out error);
if (len != 0 && error == SocketError.Success)
continue;
Console.WriteLine("连接断开");
client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
}
}
  • 打赏
  • 举报
回复
正需要看这个东西,太谢谢了
lifan185 2012-07-10
  • 打赏
  • 举报
回复
顶,楼主是个好人!!!

111,093

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • AIGC Browser
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧