C# socket编程实践——支持广播的简单socket服务器
在上篇博客简单理解socket写完之后我就希望写出一个websocket的服务器了,但是一路困难重重,还是从基础开始吧,先搞定C# socket编程基本知识,写一个支持广播的简单server/client交互demo,然后再拓展为websocket服务器。想要搞定这个需要一些基本知识
线程与进程
进程与线程对CS的同学来说肯定耳闻能像了,再啰嗦两句我个人的理解,每个运行在系统上的程序都是一个进程,进程就是正在执行的程序,把编译好的指令放入特定一块内存,顺序执行,这就是一个进程,我们平时写的if-else,for循环都按照我们预期,一步步顺序执行,这是因为我们写的是单线程的程序,所谓线程是一个进程的执行片段,我们写的单线程程序,整个进程就一个主线程,所有代码在这个线程内顺序执行,但一个进程可以有多个线程同时执行,这就是多线程程序,利用多线程支持我们可以让程序一边监听客户端请求,一边广播消息。
同步与异步
熟悉web开发的同学肯定了解这个概念,在使用ajax中我们就会用到异步的请求,同步与异步正好和我们生活中的理解相反(我尝试问过学管理的女朋友)
同步:下一个调用在上一个调用返回结果后执行,也可以理解为事情必须一件做完再去做另一件,我们经常编写的语句都是同步调用
int a=dosomething(); a+=1;
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);}}}}
View Code
然后是一个SocketMessage类,记录客户端发来的消息
public class SocketMessage{public bool isLogin { get; set; }public ClientInfo Client { get; set; }public string Message { get; set; }public DateTime Time { get; set; }}
View Code
然后定义两个全局变量记录所有客户端及所有客户端发来的消息
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[1024];//接收客户端消息client.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), client);ClientInfo info = new ClientInfo();info.Id = client.RemoteEndPoint;info.handle = client.Handle;info.buffer = buffer;//把客户端存入clientPoolthis.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, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), client);string msg = Encoding.UTF8.GetString(buffer, 0, 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 > 0){byte[] msg = PackageMessage(msgPool[0]);foreach (KeyValuePair<Socket, ClientInfo> cs in clientPool){Socket client = cs.Key;if (client.Connected){client.Send(msg, msg.Length, SocketFlags.None);}}msgPool.RemoveAt(0);}}});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(8080);}
这样我们就启用了server,看看简单的客户端实现,原理类似,不再分析了
1 class Program 2 { 3 private static byte[] buf = new byte[1024]; 4 static void Main(string[] args) 5 { 6 Console.Write("Enter your name: "); 7 string name = Console.ReadLine(); 8 Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 9 client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080)); 10 Console.WriteLine("Connected to server, enter $q to quit"); 11 name = "{<" + name.Trim() + ">}"; 12 byte[] nameBuf = Encoding.UTF8.GetBytes(name); 13 client.BeginSend(nameBuf, 0, nameBuf.Length, SocketFlags.None, null, null); 14 client.BeginReceive(buf, 0, buf.Length, SocketFlags.None, new AsyncCallback(Recieve), client); 15 while (true) 16 { 17 string msg = Console.ReadLine(); 18 if (msg == "$q") 19 { 20 client.Close(); 21 break; 22 } 23 byte[] output = Encoding.UTF8.GetBytes(msg); 24 client.BeginSend(output, 0, output.Length, SocketFlags.None, null, null); 25 } 26 Console.Write("Disconnected. Press any key to exit... "); 27 Console.ReadKey(); 28 } 29 30 private static void Recieve(IAsyncResult result) 31 { 32 try 33 { 34 Socket client = result.AsyncState as Socket; 35 int length = client.EndReceive(result); 36 string msg = Encoding.UTF8.GetString(buf, 0, length); 37 Console.WriteLine(msg); 38 client.BeginReceive(buf, 0, buf.Length, SocketFlags.None, new AsyncCallback(Recieve), client); 39 } 40 catch 41 { 42 } 43 } 44 }
View Code
有图有真相
这样一个简单的支持广播地socket就完成了,我们可以进行多个客户端聊天了,看看运行效果吧
最后
其实socket编程没有一开始我想象的那么难,重要的还是搞明白原理,接下来事情就迎刃而解了,这个简单的server还有不少待完善之处,主要是展示一下C# socket编程基本使用,为下一步做websocket server做准备,实习两者很相似,只是websocket server 添加了协议处理部分,这两天会尽快分享出来
感兴趣的同学可以看看源码 (注释是我写博客的时候加上的,源码中没有,不管看过博客的人应该没问题)
转载于:https://www.cnblogs.com/dolphinX/p/3462496.html
C# socket编程实践——支持广播的简单socket服务器相关推荐
- Socket编程实践(10) --select的限制与poll的使用
select的限制 用select实现的并发服务器,能达到的并发数一般受两方面限制: 1)一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n(number)来调整或 ...
- Socket编程实践(6) --TCP服务端注意事项
僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 sign ...
- java socket编程 聊天_基于java的socket简单聊天编程
socket编程: 一:什么是socket:socket是BSD UNIX的通信机制,通常称为"套接字",其英文原意是"孔"或"插座".有些 ...
- Socket编程实践(11) --epoll原理与封装
常用模型的特点 Linux 下设计并发网络程序,有典型的Apache模型(Process Per Connection,PPC), TPC(Thread Per Connection)模型,以及 se ...
- Socket编程实践(13) --UNIX域协议
UNIX域协议 UNIX域套接字与TCP相比, 在同一台主机上, UNIX域套接字更有效率, 几乎是TCP的两倍(由于UNIX域套接字不需要经过网络协议栈,不需要打包/拆包,计算校验和,维护序号和应答 ...
- linux网络编程IPv6socket,简单的IPv6 UDP/TCP socket编程 -- 两台Linux实现简单的ipv6通信...
配置: 1.两台linux用网线直接相连 2.手动配置两台linux的ipv6地址为: ifconfig eth0 add 2001:da8:e000::1:1:1 ifconfig eth0 add ...
- windows socket网络编程一:最简单的服务器和客户端搭建
文章目录 简介 服务器 网络版本 1.打开网络库 2.校验版本 3.创建socket 4.绑定地址和端口 5.监听 6.接受链接 7.与客户端收发消息 客户端 1.打开网络库 2.校验版本 3.创建s ...
- 网页服务器编程,50行代码实现简单网站服务器
本系列教程将和您一起探讨如何利用Java语言一步一步实现自己的web服务器. 1 需求描述 第1讲我们要实现的效果是:当我们在浏览器的地址栏输入localhost:8080的时候,我们会看到" ...
- Java:socket服务端,socket服务端支持多连接,socket客户端,socket客户端支持发送和接受
一.Java之socket服务端 新建一个Java工程 命名 给他先创建一个类 在类里面我们做一个main 这里面也需要,创建套接字,IP号,端口号 但是java中有一个类 Serve ...
最新文章
- 使用seafile搭建自己的百度云
- 点击文本框搜索,出现在下拉列表中
- TensorFlow tf.data 导入数据(tf.data官方教程) * * * * *
- 译 | 使用Roslyn分析器高效编写更好的代码
- 从零打造 Vue 聊天组件
- Python入门之软件开发目录规范
- C++操作符operator的另一种用法
- 拦截器和过滤器之间的区别
- GODOT游戏编程001
- 主成分分析(PCA)步骤及代码
- python自动办公 pdf_别再问如何用 Python 提取 PDF 内容了!
- android ocr 身份证识别
- 文字转语音软件哪个好?看完这篇你就知道了
- 爬虫(21)crawlspider讲解古诗文案例补充+小程序社区案例+汽车之家案例+scrapy内置的下载文件的方法
- mixins(混入)
- [Python]网络爬虫(六) 一个刷投票小脚本
- HTML Parser Jsoup - 网页抓取百度百科信息的例子
- 从中国500年前文人的角度重识只狼(sekiro)的场景设计
- #sora#笔记——工作流
- 汇川中型PLC与威纶通触摸屏标签通讯
热门文章
- ecshop安装linux7,RedHat下如何搭建ecshop?
- matlab 报错 javax,[求助]安装报错,求大佬帮忙
- c3074 无法使用带圆括号的_助力带分类简介
- mqtt android简书,iOS MQTT协议笔记
- matlab中的数值计算,MATLAB数值计算(中译本,最新修订)
- c语言pushback用法,C语言:【动态顺序表】动态顺序表的初始化、打印、尾插PushBack、尾删PopBack...
- 【带你重拾Redis】Redis常见知识点
- Python基础教学系列— 基础语法
- 服务器修改mime类型,服务器上设置mime类型
- java接口如何定义常量 c_在Java接口中怎样访问定义的常量呢?