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信息

View Code

 

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

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;
                //把客户端存入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, 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,看看简单的客户端实现,原理类似,不再分析了

View Code

 

 有图有真相

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

最后

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

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

 

     本文转自魏琼东博客园博客,原文链接:http://www.cnblogs.com/dolphinX/p/3462496.html,如需转载请自行联系原作者




相关文章
|
5月前
|
C# 开发者
C# 一分钟浅谈:Socket 编程基础
【10月更文挑战第7天】本文介绍了Socket编程的基础知识、基本操作及常见问题,通过C#代码示例详细展示了服务器端和客户端的Socket通信过程,包括创建、绑定、监听、连接、数据收发及关闭等步骤,帮助开发者掌握Socket编程的核心技术和注意事项。
165 3
C# 一分钟浅谈:Socket 编程基础
|
2月前
|
网络协议 C# 开发工具
C#中简单Socket编程
1. 先运行服务器代码。服务器将开始监听指定的IP和端口,等待客户端连接。 1. 然后运行客户端代码。客户端将连接到服务器并发送消息。 1. 服务器接收到消息后,将回应客户端,并在控制台上显示接收到的消息。 1. 客户端接收到服务器的回应消息,并在控制台上显示。
150 15
|
5月前
|
消息中间件 网络协议 C#
C#使用Socket实现分布式事件总线,不依赖第三方MQ
`CodeWF.EventBus.Socket` 是一个轻量级的、基于Socket的分布式事件总线系统,旨在简化分布式架构中的事件通信。它允许进程之间通过发布/订阅模式进行通信,无需依赖外部消息队列服务。
C#使用Socket实现分布式事件总线,不依赖第三方MQ
|
5月前
|
Python
Socket学习笔记(二):python通过socket实现客户端到服务器端的图片传输
使用Python的socket库实现客户端到服务器端的图片传输,包括客户端和服务器端的代码实现,以及传输结果的展示。
233 3
Socket学习笔记(二):python通过socket实现客户端到服务器端的图片传输
|
5月前
|
JSON 数据格式 Python
Socket学习笔记(一):python通过socket实现客户端到服务器端的文件传输
本文介绍了如何使用Python的socket模块实现客户端到服务器端的文件传输,包括客户端发送文件信息和内容,服务器端接收并保存文件的完整过程。
274 1
Socket学习笔记(一):python通过socket实现客户端到服务器端的文件传输
|
5月前
|
测试技术 C# 数据库
C# 一分钟浅谈:测试驱动开发 (TDD) 实践
【10月更文挑战第18天】测试驱动开发(TDD)是一种软件开发方法论,强调先编写测试代码再编写功能代码,以确保代码质量和可维护性。本文从 TDD 的基本概念入手,详细介绍了其核心步骤——编写测试、运行测试并失败、编写代码使测试通过,以及“红绿重构”循环。文章还探讨了 TDD 的优势,包括提高代码质量、促进设计思考、减少调试时间和文档化。此外,文中分析了常见问题及解决方案,如测试覆盖率不足、测试代码过于复杂、忽视重构和测试依赖过多,并通过一个简单的计算器类的代码案例,展示了 TDD 的实际应用过程。
77 1
|
5月前
|
存储 JSON API
HTTP 请求与响应处理:C#中的实践
【10月更文挑战第4天】在现代Web开发中,HTTP协议至关重要,无论构建Web应用还是API开发,都需要熟练掌握HTTP请求与响应处理。本文从C#角度出发,介绍HTTP基础知识,包括请求与响应结构,并通过`HttpClient`库演示如何发送GET请求及处理响应,同时分析常见错误并提供解决方案,助你更高效地完成HTTP相关任务。
196 2
|
5月前
|
数据采集 C# 数据库
数据验证与错误处理:C#中的实践
【10月更文挑战第1天】在软件开发中,数据验证与错误处理至关重要,不仅能提升程序的健壮性和安全性,还能改善用户体验。本文从基础概念入手,详细介绍了C#中的数据验证方法,包括使用自定义属性和静态方法验证数据,以及常见的错误处理技巧,如Try-Catch-Finally结构和自定义异常。通过具体示例,帮助读者掌握最佳实践,构建高质量应用。
218 3
|
6月前
|
SQL 开发框架 安全
并发集合与任务并行库:C#中的高效编程实践
在现代软件开发中,多核处理器普及使多线程编程成为提升性能的关键。然而,传统同步模型在高并发下易引发死锁等问题。为此,.NET Framework引入了任务并行库(TPL)和并发集合,简化并发编程并增强代码可维护性。并发集合允许多线程安全访问,如`ConcurrentQueue&lt;T&gt;`和`ConcurrentDictionary&lt;TKey, TValue&gt;`,有效避免数据不一致。TPL则通过`Task`类实现异步操作,提高开发效率。正确使用这些工具可显著提升程序性能,但也需注意任务取消和异常处理等常见问题。
84 1
|
5月前
|
开发框架 缓存 算法
开源且实用的C#/.NET编程技巧练习宝库(学习,工作,实践干货)
开源且实用的C#/.NET编程技巧练习宝库(学习,工作,实践干货)
375 0