• C# winform websocket学习笔记(二)winform服务端


    1 功能设计

    与连接的客户端同步a,b的值,具体来说:

    1.服务端开启后,可以被客户端进行连接;

    2.客户端向服务端发送数据,服务端接收数据后将其转发推送给所有连接的客户端。应用场景是,客户端想要修改远程服务器数据库,向服务端发送信息,服务端自身或者通过其他程序修改之后,将变更后的数据推送给所有连接的客户端;

    3.当有新的客户端连接进来,不必等到数据变化,服务端主动将当前最新数据推送给这个客户端;

    4.发送的数据要约定好规则进行解析,否则参数一多就会乱了套。不过这里为了省事我暂且只将其包装成json形式。

    文末提供源码下载。

    2 界面设计

     界面将就着看吧

    2.1 界面说明

    大致可分为:

    监听地址设置;

    监听开启/关闭,及服务端状态标签;

    a,b值的显示与广播;

    信息框,用于将一些信息显示出来。

    3 主要代码实现

    3.1 命名空间及引用

    using Newtonsoft.Json;
    using System.Threading;
    using System.Web;
    using System.Net.WebSockets;
    using System.Net;
    using System.Diagnostics;//AddAddress方法使用

    在默认生成的using后面添加这些。

    注意Newtonsoft.Json适用于处理json,需要将这个dll文件添加进引用,可以右键引用-管理nuget程序包搜索,也可以 工具-nuget包管理器-程序包管理控制台 输入

    Install-Package Newtonsoft.Json

    3.1 开启监听

    实例化httplistener对象监听端口,如果是websocket类型请求,则进行处理

    //开启监听
            private void btnStart_Click(object sender, EventArgs e)
            {
                if (txtIPAddress.Enabled == true)
                {
                    MessageBox.Show("请先确认地址");
                    return;
                }
                string IpAdress = txtIPAddress.Text;
                txtInfo.AppendText("打开监听" + DateTime.Now.ToString() + "
    ");
                Start(IpAdress);
            }
    View Code
    //存储当前所有连接的静态列表
            private static List<WebSocket> _sockets = new List<WebSocket>();
    
            /// <summary>
            /// 对参数地址进行监听,如果是websocket请求,转入相应方法
            /// </summary>
            /// <param name="httpListenerPrefix">http监听地址,记得以 / 结尾</param>
            public async void Start(string httpListenerPrefix)
            {
                try
                {
                    //HttpListener httpListener = new HttpListener();//改为静态对象,方便关闭
                    httpListener.Prefixes.Add(httpListenerPrefix);
                    //通过连接名称可以区分多个websocket服务。如可以通过 http://localhost:8080/learn http://localhost:8080/work 使用两个服务,不过需要多线程和两个http监听对象等
                    httpListener.Start(); 
                    lblListen.Text = "listening...";
                    while (true)
                    {
                        //http端口监听获取内容
                        HttpListenerContext httpListenerContext = await httpListener.GetContextAsync();
                        if (httpListenerContext.Request.IsWebSocketRequest)//如果是websocket请求
                        {
                            //进入此方法
                            ProcessRequest(httpListenerContext);
                        }
                        else
                        {
                            httpListenerContext.Response.StatusCode = 400;
                            httpListenerContext.Response.Close();
                        }
                    }
                }
                catch (Exception ex)
                {
                    txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "
    ");
    
                }                     
            }
    View Code

    3.2 数据处理

    进行请求处理之前,先约定好数据格式

    /// <summary>
            /// //以dictionary将数据的键值对匹配,然后进行json序列化,避免定义类的麻烦。
            /// </summary>
            /// <param name="valueA">a的值</param>
            /// <param name="valueB">b的值</param>
            /// <returns></returns>
            public static string SerializeJson(string valueA, string valueB)
            {
                if (valueA.Length == 0) { valueA = "-"; }
                if (valueB.Length == 0) { valueB = "-"; }
                //以dictionary将数据的键值对匹配,然后进行json序列化,避免定义类的麻烦。参考:https://www.cnblogs.com/kevinWu7/p/10163455.html
                Dictionary<string, string> dic = new Dictionary<string, string>(){
                    { "a",valueA },
                    { "b",valueB }
                };
                string Jsondata = JsonConvert.SerializeObject(dic);
                return Jsondata;
            }
    View Code
        //用于json反序列化获取实体
        public class TestValue
        {
            public string a { get; set; }
            public string b { get; set; }
        }

    3.3 处理请求

     先通过webSocketContext从传入参数中读取内容,然后通过WebSocket webSocket = webSocketContext.WebSocket;获取websocket连接。

    将连接加入到一个静态list列表中,用于向所有客户端广播。(其实这里可以用dictionary等来存储,并且可以将连接的一些其他信息比如IP等一起放进去)

    对这一个新接入连接,向其同步最新数据。

    当此连接状态是打开情况,准备一些数组之类,用于接收读取数据。

    通过webSocket.ReceiveAsync()进行异步接收。

    将读取到的字节数组转成字符串,由于客户端发送的是序列化后的json字符串,因此需要反序列化转成对象实例,获取a,b的值,将其显示到文本框。

    刷新判断一下静态列表中连接的状态,清理无效连接,将之前的字节数组发送给剩余websocket连接。

    /// <summary>
            /// 处理端口监听到的请求
            /// </summary>
            /// <param name="httpListenerContext"></param>
            private async void ProcessRequest(HttpListenerContext httpListenerContext)
            {
                //WebSocketContext 类用于访问websocket握手中的信息
                WebSocketContext webSocketContext = null;
                try
                {
                    webSocketContext = await httpListenerContext.AcceptWebSocketAsync(subProtocol: null);
                    //获取客户端IP
                    string ipAddress = httpListenerContext.Request.RemoteEndPoint.Address.ToString();
                    txtInfo.AppendText("connected:IPAddress" + ipAddress + "
    ");
                }
                catch (Exception e)//如果出错
                {
                    httpListenerContext.Response.StatusCode = 500;
                    httpListenerContext.Response.Close();
                    txtInfo.AppendText("Exception:" + e.ToString() + DateTime.Now.ToString() + "
    ");
                    return;
                }
                //获取websocket连接
                WebSocket webSocket = webSocketContext.WebSocket;
                _sockets.Add(webSocket);//此处将web socket对象加入一个静态列表中
                SendToNewConnection(webSocket);//将当前服务器上最新的数据(a,b的值)发送过去
    
                try
                {
                    //我们定义一个常数,它将表示接收到的数据的大小。 它是由我们建立的,我们可以设定任何值。 我们知道在这种情况下,发送的数据的大小非常小。
                    const int maxMessageSize = 2048;
                    //received bits的缓冲区
    
                    while (webSocket != null && webSocket.State == WebSocketState.Open)//如果连接是打开的
                    {   
                        //此句放在while里面,每次使用都重新初始化。如果放在外面,由于没有进行清空操作,下一次接收的数据若比上一次短,则会多出一部分内容。
                        var receiveBuffer = new ArraySegment<Byte>(new Byte[maxMessageSize]);
    
                        WebSocketReceiveResult receiveResult=null;
                        byte[] payloadData = null;
                        do
                        {
                            //读取数据。此类的实例表示在 WebSocket 上执行单个 ReceiveAsync 操作所得到的结果
                            receiveResult = await webSocket.ReceiveAsync(receiveBuffer, CancellationToken.None);
                            //字节数组
                            payloadData = receiveBuffer.Array.Where(b => b != 0).ToArray();
                        }
                        while (!receiveResult.EndOfMessage);//如果指示已完整接收消息则停止
                        
                        //如果输入帧为取消帧,发送close命令。
                        //MessageType指示当前消息是utf-8消息还是二进制信息。Text(0,明文形式),Close(2,收到关闭消息,接受已完成),Binary(1,消息采用二进制格式)
                        if (receiveResult.MessageType == WebSocketMessageType.Close)
                        {
                            await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, CancellationToken.None);
                            _sockets.Remove(webSocket);//从列表移除当前连接
    
                        }
                        else
                        {                        
                            //因为我们知道这是一个字符串,我们转换它                        
                            string receiveString = System.Text.Encoding.UTF8.GetString(payloadData, 0, payloadData.Length);
    
                            try//将反序列化内容放入try中,避免无法匹配、内容为空等可能报错的地方
                            {
                                //将转换后的字符串内容进行json反序列化。参考:https://www.cnblogs.com/yinmu/p/12160343.html
                                TestValue tv = JsonConvert.DeserializeObject<TestValue>(receiveString);
                                //将收到的a,b的值显示到文本框
                                if (tv != null)
                                {
                                    string valueA = string.Empty, valueB = string.Empty;
                                    if (tv.a != null && tv.a.Length > 0) { valueA = tv.a; }
                                    if (tv.a != null && tv.b.Length > 0) { valueB = tv.b; }
                                    txtAvalue.Text = valueA;
                                    txtBvalue.Text = valueB;
                                }
                                
                                RefreshConnectionList();//先清理无效的连接,否则会导致服务端websocket被dispose
                                //当接收到文本消息时,对当前服务器上所有web socket连接进行广播
                                foreach (var innerSocket in _sockets)
                                {
                                    await innerSocket.SendAsync(new ArraySegment<byte>(payloadData), WebSocketMessageType.Text, true, CancellationToken.None);
                                }
                            }
                            catch (Exception ex)
                            {
                                //如果json反序列化出了问题
                                txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "
    ");//将错误类型显示出来
                                txtInfo.AppendText(receiveString + DateTime.Now.ToString() + "
    ");//将收到的原始字符串显示出来
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    if (e.GetType().ToString() == "System.Net.WebSockets.WebSocketException")
                    {
                        //客户端关闭时会抛出此错误
                        txtInfo.AppendText("a connection closed" + DateTime.Now.ToString() + "
    ");
                    }
                    else
                    {
                        txtInfo.AppendText(e.ToString() + DateTime.Now.ToString() + "
    ");
                    }
                }
            }
    View Code

    4 对websocket连接的处理

    4.1 广播

    /// <summary>
            /// 服务端主动向所有客户端广播
            /// </summary>
            /// <param name="jsonmessage">传过来的应该是序列化后的json字符串,接收方会通过TestValue类进行反序列化获取a,b的内容</param>
            public  async void Broadcast(string jsonmessage)
            {
                try
                {
                    Byte[] bytes = System.Text.Encoding.UTF8.GetBytes(jsonmessage);
                    RefreshConnectionList();//先清理无效的连接,否则会导致服务端websocket被dispose
                    //当接收到文本消息时,对当前服务器上所有web socket连接进行广播
                    foreach (var innerSocket in _sockets)
                    {                
                        await innerSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
                    }
                }
                catch (Exception ex)
                {
                    txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "
    ");
                    MessageBox.Show("某些连接出了问题,如果广播多次出问题,请重启服务端");
                }
                
            }
    View Code

    4.2 向新连接客户端同步数据

    /// <summary>
            /// 监听到一个新的websocket连接后,将服务器当前最新数据同步过去
            /// </summary>
            /// <param name="currentWebsocket">当前新接入的websocket连接</param>
            public async void SendToNewConnection(WebSocket currentWebsocket)
            {
                try
                {
                    string jsonmessage = SerializeJson(txtAvalue.Text, txtBvalue.Text);
                    Byte[] bytes = System.Text.Encoding.UTF8.GetBytes(jsonmessage);
    
                    if (currentWebsocket.State == WebSocketState.Open)
                    {
                        await currentWebsocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);//try访问已释放对象问题
                    }
                    else
                    {
                        //此处并不对该链接进行移除,会导致调用本方法后的代码出问题,只需在进行发送时确认状态即可
                        txtInfo.AppendText("新接入连接:" + currentWebsocket.GetHashCode().ToString() + "的连接状态不为open,因此停止向其同步数据" + DateTime.Now.ToString() + "
    ");
                    }
                }
                catch (Exception ex)
                {
                    txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "
    ");
                }
            }
    View Code

    4.3 清理连接

    /// <summary>
            /// 刷新当前websocket连接列表,如果状态为Closed,则移除。连接异常断开后会被dispose,如果访问会报错,但可以获取状态为closed
            /// </summary>
            public static void RefreshConnectionList()
            {
                if (_sockets != null)//lock不能锁定空值
                {
                    lock (_sockets)//锁定数据源
                    {
                        //System.InvalidOperationException: 集合已修改;可能无法执行枚举操作。
                        //使用foreach不能执行删除、修改,这是规定。你可以使用for循环遍历修改。删除数据正确做法,for循环 i 要从大到小
                        for (int i = _sockets.Count-1; i >=0; i--)
                        {
                            if (_sockets[i].State != WebSocketState.Open)
                            {
                                _sockets.Remove(_sockets[i]);
                            }
                        }
                    }
                }
                
            }
    View Code

    5 权限问题

    5.1 问题介绍

    服务端客户端完成后,VS调试正常,如果想在局域网下测试,结果发现服务端报httplistener拒绝访问,客户端报无法连接远程主机,即使关闭防火墙也没用。

    这是因为这个执行程序没有获得权限来监听。有以下几种方法。

    不过不管哪种方法都需要添加服务端电脑的监听端口的入站规则,或者关闭防火墙。添加入站规则可见2.

    5.2 管理员权限运行

    这是最简单粗暴的,直接右键以管理员运行即可

    5.3 cmd添加

    参考:https://blog.csdn.net/chenludaniel/article/details/79720024

    5.4 提升权限

    参考:https://www.cnblogs.com/cmdszh/archive/2012/08/16/httplistener.html

    不过第四种方法经过实测,由于通过clickonce添加的manifest配置文件无法获取管理员权限,导致此种方法无效。不过原理上和3是一致的。

    6 功能测试 

    测试一个服务端与三个客户端,winform服务端与两个winform客户端在一台电脑上,一个HTML页web客户端在局域网手机浏览器上。(注意,服务端以管理员身份打开)

     

     点我下载源码

  • 相关阅读:
    XML案例
    4.6Java数组的遍历
    4.3Java多态(polymorphism)
    XML文档的标准
    4.6Java数组的定义
    4.6Java数组初始化的方式
    XML介绍
    4.6Java对象转型(casting)
    HelloWorld之MyBatis
    Hibernate查询方法比较
  • 原文地址:https://www.cnblogs.com/ygxddxc/p/13215510.html
Copyright © 2020-2023  润新知