在上篇博客简单理解socket写完之后我就希望写出一个websocket的服务器了,但是一路困难重重,还是从基础开始吧,先搞定C# socket编程基本知识,写一个支持广播的简单server/client交互demo,然后再拓展为websocket服务器。想要搞定这个需要一些基本知识

线程与进程

进程与线程对CS的同学来说肯定耳闻能像了,再啰嗦两句我个人的理解,每个运行在系统上的程序都是一个进程,进程就是正在执行的程序,把编译好的指令放入特定一块内存,顺序执行,这就是一个进程,我们平时写的if-else,for循环都按照我们预期,一步步顺序执行,这是因为我们写的是单线程的程序,所谓线程是一个进程的执行片段,我们写的单线程程序,整个进程就一个主线程,所有代码在这个线程内顺序执行,但一个进程可以有多个线程同时执行,这就是多线程程序,利用多线程支持我们可以让程序一边监听客户端请求,一边广播消息。

同步与异步

熟悉web开发的同学肯定了解这个概念,在使用ajax中我们就会用到异步的请求,同步与异步正好和我们生活中的理解相反(我尝试问过学管理的女朋友)

同步:下一个调用在上一个调用返回结果后执行,也可以理解为事情必须一件做完再去做另一件,我们经常编写的语句都是同步调用

int a=dosomething();
a+=;

a+=1; 这条指令必须在dosomething()方法执行完毕返回结果后才可以执行,否则就乱了套

异步:异步概念和同步相对,当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者(百度上抄的)。理解了同步概念后异步也就不难理解了,以javascript的ajax为例

ajax(arg1,arg2,function(){
//回调函数
  a=3;
});
a=4;

这个代码段执行完成后一般情况会把a赋值为3而不是4,因为在ajax方法调用后,a=4;这条语句并没有等待ajax()返回结果就执行了,也就是在ajax()执行完成调用回调函数之前,a=4;已经执行了,回调函数再把a赋值为3使之成为最后结果,为此在ajax调用中我们经常会使用回调函数,其实在很多异步处理中我们都会使用到回调函数。

阻塞

阻塞操作是指,在执行设备操作时,若不能获得资源,则进程挂起直到满足可操作的条件再进行操作。

步骤

了解了上面知识我们就可以按照下图来写我们的服务器了

整体结构

关于怎么具体一步步使用socket我就不说了,有兴趣同学可以看看你得学会并且学得会的Socket编程基础知识,看看我们服务器的结构,我写了一个TcpHelper类来处理服务器操作

首先定义 一个ClientInfo类存放Client信息

public class ClientInfo
{
public byte[] buffer;
public string NickName { get; set; }
public EndPoint Id { get; set; }
public IntPtr handle { get; set; }
public string Name
{
get
{
if (!string.IsNullOrEmpty(NickName))
{
return NickName;
}
else
{
return string.Format("{0}#{1}", Id, handle);
}
}
}
}

然后是一个SocketMessage类,记录客户端发来的消息

public class SocketMessage
{
public bool isLogin { get; set; }
public ClientInfo Client { get; set; }
public string Message { get; set; }
public DateTime Time { get; set; }
}

然后定义两个全局变量记录所有客户端及所有客户端发来的消息

private Dictionary<Socket, ClientInfo> clientPool = new Dictionary<Socket, ClientInfo>();
private List<SocketMessage> msgPool = new List<SocketMessage>();

然后就是几个主要方法的定义

        /// <summary>
/// 启动服务器,监听客户端请求
/// </summary>
/// <param name="port">服务器端进程口号</param>
public void Run(int port); /// <summary>
/// 在独立线程中不停地向所有客户端广播消息
/// </summary>
private void Broadcast(); /// <summary>
/// 把客户端消息打包处理(拼接上谁什么时候发的什么消息)
/// </summary>
/// <returns>The message.</returns>
/// <param name="sm">Sm.</param>
private byte[] PackageMessage(SocketMessage sm); /// <summary>
/// 处理客户端连接请求,成功后把客户端加入到clientPool
/// </summary>
/// <param name="result">Result.</param>
private void Accept(IAsyncResult result); /// <summary>
/// 处理客户端发送的消息,接收成功后加入到msgPool,等待广播
/// </summary>
/// <param name="result">Result.</param>
private void Recieve(IAsyncResult result);

逐个分析一下把

void run(int port)

这是该类唯一提供的共有方法,供外界调用,来根据port参数创建一个socket

public void Run(int port)
{
Thread serverSocketThraed = new Thread(() =>
{
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
server.Bind(new IPEndPoint(IPAddress.Any, port));
server.Listen(10);
server.BeginAccept(new AsyncCallback(Accept), server);
}); serverSocketThraed.Start();
Console.WriteLine("Server is ready");
Broadcast();
}

代码很简单,需要注意的有几点

1.在一个新线程中创建服务器socket,最多允许10个客户端连接。

2.在方法最后调用Broadcast()方法用于向所有客户端广播消息

3.BeginAccept方法,MSDN上有权威解释,但是觉得不够接地气,简单说一下我的理解,首先这个方法是异步的,用于服务器接受一个客户端的连接,第一个参数实际上是回调函数,在C#中使用委托,在回调函数中通过调用EndAccept就可以获得尝试连接的客户端socket,第二个参数是包含请求state的对象,传入server socket对象本身就可以了

void Accept(IAsyncResult result)

方法用于处理客户端连接请求

private void Accept(IAsyncResult result)
{
Socket server = result.AsyncState as Socket;
Socket client = server.EndAccept(result);
try
{
//处理下一个客户端连接
server.BeginAccept(new AsyncCallback(Accept), server);
byte[] buffer = new byte[];
//接收客户端消息
client.BeginReceive(buffer, , buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), client);
ClientInfo info = new ClientInfo();
info.Id = client.RemoteEndPoint;
info.handle = client.Handle;
info.buffer = buffer;
//把客户端存入clientPool
this.clientPool.Add(client, info);
Console.WriteLine(string.Format("Client {0} connected", client.RemoteEndPoint));
}
catch (Exception ex)
{
Console.WriteLine("Error :\r\n\t" + ex.ToString());
}
}

BeginRecieve方法的MSDN有解释,和Accept一样也是异步处理,接收客户端消息,放入第一个参数中,它也传入了一个回调函数的委托,和带有socket state的对象,用于处理下一次接收。我们把接收成功地客户端socket及其对应信息存放到clientPool中

void Recieve(IAsyncResult result)

方法用于接收客户端消息,并把所有消息及其发送者信息存入msgInfo,等待广播

private void Recieve(IAsyncResult result)
{
Socket client = result.AsyncState as Socket; if (client == null || !clientPool.ContainsKey(client))
{
return;
} try
{
int length = client.EndReceive(result);
byte[] buffer = clientPool[client].buffer; //接收消息
client.BeginReceive(buffer, , buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), client);
string msg = Encoding.UTF8.GetString(buffer, , length);
SocketMessage sm = new SocketMessage();
sm.Client = clientPool[client];
sm.Time = DateTime.Now; Regex reg = new Regex(@"{<(.*?)>}");
Match m = reg.Match(msg);
if (m.Value != "") //处理客户端传来的用户名
{
clientPool[client].NickName = Regex.Replace(m.Value, @"{<(.*?)>}", "$1");
sm.isLogin = true;
sm.Message = "login!";
Console.WriteLine("{0} login @ {1}", client.RemoteEndPoint,DateTime.Now);
}
else //处理客户端传来的普通消息
{
sm.isLogin = false;
sm.Message = msg;
Console.WriteLine("{0} @ {1}\r\n {2}", client.RemoteEndPoint,DateTime.Now,msg);
}
msgPool.Add(sm);
}
catch
{
//把客户端标记为关闭,并在clientPool中清除
client.Disconnect(true);
Console.WriteLine("Client {0} disconnet", clientPool[client].Name);
clientPool.Remove(client);
}
}

这个的代码都很简单,就不多解释了,我加入了用户名处理用于广播客户端消息的时候显示客户端自定义的昵称而不是生硬的ip地址+端口号,当然这里需要客户端配合

Broadcast()

服务器已经和客户端连接成功,并且接收到了客户端消息,我们就可以看看该怎么广播消息了,Broadcast()方法已经在run()方法内调用,看看它是怎么运作广播客户端消息的

private void Broadcast()
{
Thread broadcast = new Thread(() =>
{
while (true)
{
if (msgPool.Count > )
{
byte[] msg = PackageMessage(msgPool[]);
foreach (KeyValuePair<Socket, ClientInfo> cs in clientPool)
{
Socket client = cs.Key;
if (client.Connected)
{
client.Send(msg, msg.Length, SocketFlags.None);
}
}
msgPool.RemoveAt();
}
}
}); broadcast.Start();
}

Broadcast()方法启用了一个新线程,循环检测msgPool是否为空,当不为空的时候遍历所有客户端,调用send方法发送msgPool里面的第一条消息,然后清除该消息继续检测,直到消息广播完,其实这就是一个阉割版的观察者模式 ,顺便看一下打包数据方法

private byte[] PackageMessage(SocketMessage sm)
{
StringBuilder packagedMsg = new StringBuilder();
if (!sm.isLogin) //消息是login信息
{
packagedMsg.AppendFormat("{0} @ {1}:\r\n ", sm.Client.Name, sm.Time.ToShortTimeString());
packagedMsg.Append(sm.Message);
}
else //处理普通消息
{
packagedMsg.AppendFormat("{0} login @ {1}", sm.Client.Name, sm.Time.ToShortTimeString());
} return Encoding.UTF8.GetBytes(packagedMsg.ToString());
}

如何使用

static void Main(string[] args)
{
TcpHelper helper = new TcpHelper();
helper.Run();
}

这样我们就启用了server,看看简单的客户端实现,原理类似,不再分析了

 class Program
{
private static byte[] buf = new byte[];
static void Main(string[] args)
{
Console.Write("Enter your name: ");
string name = Console.ReadLine();
Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), ));
Console.WriteLine("Connected to server, enter $q to quit");
name = "{<" + name.Trim() + ">}";
byte[] nameBuf = Encoding.UTF8.GetBytes(name);
client.BeginSend(nameBuf, , nameBuf.Length, SocketFlags.None, null, null);
client.BeginReceive(buf, , buf.Length, SocketFlags.None, new AsyncCallback(Recieve), client);
while (true)
{
string msg = Console.ReadLine();
if (msg == "$q")
{
client.Close();
break;
}
byte[] output = Encoding.UTF8.GetBytes(msg);
client.BeginSend(output, , output.Length, SocketFlags.None, null, null);
}
Console.Write("Disconnected. Press any key to exit... ");
Console.ReadKey();
} private static void Recieve(IAsyncResult result)
{
try
{
Socket client = result.AsyncState as Socket;
int length = client.EndReceive(result);
string msg = Encoding.UTF8.GetString(buf, , length);
Console.WriteLine(msg);
client.BeginReceive(buf, , buf.Length, SocketFlags.None, new AsyncCallback(Recieve), client);
}
catch
{
}
}
}

有图有真相

这样一个简单的支持广播地socket就完成了,我们可以进行多个客户端聊天了,看看运行效果吧

最后

其实socket编程没有一开始我想象的那么难,重要的还是搞明白原理,接下来事情就迎刃而解了,这个简单的server还有不少待完善之处,主要是展示一下C# socket编程基本使用,为下一步做websocket server做准备,实习两者很相似,只是websocket server 添加了协议处理部分,这两天会尽快分享出来

感兴趣的同学可以看看源码 (注释是我写博客的时候加上的,源码中没有,不管看过博客的人应该没问题)

C# socket编程实践——支持广播的简单socket服务器的更多相关文章

  1. C# socket编程实践

    C# socket编程实践——支持广播的简单socket服务器   在上篇博客简单理解socket写完之后我就希望写出一个websocket的服务器了,但是一路困难重重,还是从基础开始吧,先搞定C# ...

  2. Socket编程实践(10) --select的限制与poll的使用

    select的限制 用select实现的并发服务器,能达到的并发数一般受两方面限制: 1)一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n(number)来调整或 ...

  3. Socket编程实践(6) --TCP服务端注意事项

    僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 sign ...

  4. Socket编程实践(6) --TCPNotes服务器

    僵尸进程过程 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中加入 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法.解决僵尸进程 sign ...

  5. Socket编程实践(2) Socket API 与 简单例程

    在本篇文章中,先介绍一下Socket编程的一些API,然后利用这些API实现一个客户端-服务器模型的一个简单通信例程.该例子中,服务器接收到客户端的信息后,将信息重新发送给客户端. socket()函 ...

  6. Socket编程实践(11) --epoll原理与封装

    常用模型的特点 Linux 下设计并发网络程序,有典型的Apache模型(Process Per Connection,PPC), TPC(Thread Per Connection)模型,以及 se ...

  7. Socket编程实践(13) --UNIX域名协议

    UNIX域名协议 UNIX域套接字与TCP相比, 在同一台主机上, UNIX域套接字更有效率, 差点儿是TCP的两倍(因为UNIX域套接字不须要经过网络协议栈,不须要打包/拆包,计算校验和,维护序号和 ...

  8. Socket编程实践(13) --UNIX域协议

    UNIX域协议 UNIX域套接字与TCP相比, 在同一台主机上, UNIX域套接字更有效率, 几乎是TCP的两倍(由于UNIX域套接字不需要经过网络协议栈,不需要打包/拆包,计算校验和,维护序号和应答 ...

  9. Socket编程实践(1) 基本概念

    1. 什么是socket socket可以看成是用户进程与内核网络协议栈的编程接口.TCP/IP协议的底层部分已经被内核实现了,而应用层是用户需要实现的,这部分程序工作在用户空间.用户空间的程序需要通 ...

随机推荐

  1. css高级应用及问题记录(持续更新)

    css 参考手册: 1.http://css.doyoe.com/ 1.混合选择器样式定义: .button.icon:before {    content: "";    po ...

  2. SpringMVC 的RequestMapping

    Spring MVC之@RequestMapping 详解 2013-10-13 23:39 150698人阅读 评论(6) 收藏 举报  分类: Java 精华文章(161)    目录(?)[+] ...

  3. ros问题总结

    1.-- package 'orocos-bfl' not found CMake Error at /usr/share/cmake-2.8/Modules/FindPkgConfig.cmake: ...

  4. js跳转到新页面传参以及接收参数的方法

    1.传递参数: window.location.href = "./list.html?id="+id; 1.接收参数: (1)接收参数函数封装 function GetReque ...

  5. tmpfs介绍

    tmpfs 前几天发现服务器的内存(ram)和swap使用率非常低,于是就想这么多的资源不用岂不浪费了?google了一下,认识了tmpfs,总的来说tmpfs是一种虚拟内存文件系统正如这个定义它最大 ...

  6. flask_日期和时间

    不知道大家有没有发现,在我们学习flask的过程中,post的timestamp字段添加时间时一直用的是datetime.utcnow()来获取时间,但是它获取的时间跟本地时间不一样,下面我们来测试一 ...

  7. gc roots 垃圾回收

    gc roots包括以下几个: 虚拟机栈(栈桢中的本地变量表)中的引用对象 方法区中的类静态属性引用的对象 方法区中的常量引用的对象 本地方法栈中JNI(即native方法)的引用的对象 java,c ...

  8. 好文推荐系列---------JS模板引擎

    这篇文章写的很清晰,不再赘述,直接上链接:http://www.tuicool.com/articles/aiaqMn

  9. Java 元注解

    元注解@Target,@Retention,@Documented,@Inherited * * @Target 表示该注解用于什么地方,可能的 ElemenetType 参数包括: * Elemen ...

  10. GIT分支管理模型

    GIT分支管理模型 link: git-branching-model 主分支(Main branches) 项目两个常驻分支: master 主干分支(锁定),仅用于发布新版本,平时不能在上面干活, ...