C# 回家作业(1)

最近都在全台跑面试

都没时间继续写..

刚好面试某金控

面试官出了个回家作业给我

就花了一个下午把它完成 顺便做为这次的主题

需求如下

写个Winform(Client)

利用GridView显示100笔股票资料(名称/成交价/涨跌/涨跌幅)

涨红跌绿平黄

另外要写个Server(Console)

client去取server资料更新

频率..原本面试官是说一秒一次 後来也没有特定限制 所以我写个参数供使用者自行设定

直接上原始码

https://github.com/bantime/CodeReview/tree/main/StockHomeWork

既然要写TCP连线,就先写个Lib来用,如果以後有需要也能直接使用

首先是Server端

public class TcpSocketServer
{
    private readonly IPAddress _IPAddress;
    private readonly int _Port;
    private Socket _Listener;
    public TcpSocketServer(IPAddress IPAddress, int Port)
    {
        _IPAddress = IPAddress;
        _Port = Port;
    }
    private List<SocketObj> Clients { get; set; } = new List<SocketObj>();
    public event Action<SocketObj> OnClientAccept;
    
    public async Task StartListening()
    {
        try
        {
            IPEndPoint localEndPoint = new IPEndPoint(_IPAddress, _Port);
            _Listener = new Socket(AddressFamily.InterNetwork,
                                    SocketType.Stream, ProtocolType.Tcp);
    
            _Listener.Bind(localEndPoint);
            _Listener.Listen();
    
            while (true)
            {
                var socket = await _Listener.AcceptAsync();
    
                var clientObj = new SocketObj(socket);
                OnClientAccept?.Invoke(clientObj);
    
                clientObj.OnDisconnect += (co) =>
                {
                    Clients.Remove(co);
                    Console.WriteLine("Client Disconnect!");
                };
                Clients.Add(clientObj);
                Console.WriteLine("New Client Accept!");
            }
    
        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }
        finally
        {
            Console.WriteLine("Server Closeing!");
        }
    }
}

主要核心为 StartListening这个方法

while回圈前就只是设定要bind的ip/port

注意如果bind的port有其他程序已经先用了会出Exception

所以请稍微选一下port 不要用常用的(要冲到也有难度

再来是while回圈

这里是等待client接入 程序会block在var socket = await _Listener.AcceptAsync();这行

如果没有任何client接入的话 不会继续往下执行

所以此时如果有client接入我会呼叫OnClientAccept这个事件(委派的用法之前有说明过 有兴趣可以回去翻

然後加入client的集合 设定断线後要从集合移除的事件

然後继续等待下一个client接入(while回圈

这里有用到一个类别 SocketObj 这是我自己写的 程序码如下 主要是提供接收讯息 及 送出讯息的方法

所以我可以从client的集合中挑出我要发送讯息的对象针对该client发送讯息

public class SocketObj
    {
        protected Socket Socket { get; set; }
        private ProtocolComplete ProtocolComplete { get; set; }
        public event Action<byte[]> OnDataReceive;
        public event Action<SocketObj> OnDisconnect;
        /// <summary>
        /// 送出资料前加上资料长度的Head
        /// </summary>
        /// <param name="datas"></param>
        /// <returns></returns>
        public async Task SendAsync(byte[] datas)
        {
            var data = new ArraySegment<byte>(datas.GetDataWithHead());
            await Socket.SendAsync(data, SocketFlags.None);
        }
        /// <summary>
        /// 不预先计算资料长度 直接送出
        /// </summary>
        /// <param name="datas"></param>
        /// <returns></returns>
        public async Task SendAsyncNotSetHead(byte[] datas)
        {
            try
            {
                var data = new ArraySegment<byte>(datas);
                await Socket.SendAsync(data, SocketFlags.None);
            }
            catch(Exception e)
            {
                OnDisconnect?.Invoke(this);
            }
        }

        public SocketObj(Socket socket)
        {
            this.Socket = socket;
            ProtocolComplete = new ProtocolComplete();
            ProtocolComplete.CompleteProtocolEvent += (datas) => OnDataReceive?.Invoke(datas);
        }

        public async Task StartReceiveAsync()
        {
            var buffer = new byte[2048];
            var data = new ArraySegment<byte>(buffer);

            while (true)
            {
                try
                {
                    var count = await Socket.ReceiveAsync(data, SocketFlags.None);
                    ProtocolComplete.ReceiveData(buffer.Take(count));
                }
                catch (Exception e)
                {
                    OnDisconnect?.Invoke(this);
                    //TODO 断线
                    return;
                }
            }
        }
    }

这里的核心是StartReceiveAsync这个方法 会持续接收资料跟Server的等待接入有点像

如果都没有资料传来 会卡在var count = await Socket.ReceiveAsync(data, SocketFlags.None);这行

如果有资料传来 我就会将资料传入ProtocolComplete.ReceiveData(buffer.Take(count));

附带一提 count 是这次接收到多少资料 然後继续跑while回圈继续等待资料

如果断线会出Exception 就呼叫断线事件就好 这边就看断线想干嘛

这里面有用到ProtocolComplete这个类别

这个类别主要是我用来处理封包不完整的问题

以前曾经遇到过

client送出讯息 [1,2,3,4,5,6,7,8,9,10]

server先接收到 [1,2,3]

然後再接收到[4,5,6,7,8,9,10]

因此我这里自己定义 所有封包讯息送出以前

前面会利用4个byte(1个uint的大小) 纪录我需要读取多少长度才算完整

当我socket持续收到资料 就会先塞给ProtocolComplete的ReceiveData

里面会暂存目前的资料TempData

如果长度有至少四个 就代表我可以知道我接下来要读取多长的资料才算完整封包

记录在ReceiveLength上(如果是-1代表我尚未得知接下来要读取多长

当我得知要读取的长度後且TempData的资料长度够了

我就将该资料取出 并呼叫 CompleteProtocolEvent这个事件给事件提供者

代表这个byte[]是完整的 可以进行处理了 并将ReceiveLength设置为-1 进行下一次完整封包判断

程序码如下

public class ProtocolComplete
    {
        public event Action<byte[]> CompleteProtocolEvent;
        private readonly List<byte> TempData = new List<byte>();
        private int ReceiveLength = -1;
        public void ReceiveData(IEnumerable<byte> Data)
        {
            TempData.AddRange(Data);
            if (ReceiveLength < 0 && TempData.Count >= 4)
            {
                ReceiveLength = BitConverter.ToInt32(TempData.GetRange(0, 4).ToArray(), 0);
                TempData.RemoveRange(0, 4);
            }
            if (ReceiveLength >= 4)
            {
                if (ReceiveLength <= TempData.Count)
                {
                    byte[] aFullData = TempData.GetRange(0, ReceiveLength).ToArray();
                    TempData.RemoveRange(0, ReceiveLength);
                    if (CompleteProtocolEvent != null)
                        CompleteProtocolEvent(aFullData);
                    ReceiveLength = -1;
                }
            }
            else
            {
                throw new Exception("ReceiveLength Error! " + ReceiveLength);
            }
        }
    }

所以 SocketObj在实例化的时候 就会注册ProtocolComplete.CompleteProtocolEvent事件

ProtocolComplete.CompleteProtocolEvent += (datas) => OnDataReceive?.Invoke(datas);

会将data再抛给OnDataReceive事件

SocketObj会用在两个地方 一个是Server接收到接入请求时,另外一个是Client

client还需要连接server的方法 但是大部分功能都跟SocketObj相同 於是我采用继承关系

程序码如下

public class TcpSocketClient : SocketObj
    {

        private readonly IPAddress _IPAddress;
        private readonly int _Port;

        public int ConnectTimeOut = 10;
        public TcpSocketClient(IPAddress IPAddress, int Port) : base(new Socket(AddressFamily.InterNetwork,
                                      SocketType.Stream, ProtocolType.Tcp))
        {
            _IPAddress = IPAddress;
            _Port = Port;
        }

        public TcpSocketClient(string IPString, int Port) : base(new Socket(AddressFamily.InterNetwork,
                                      SocketType.Stream, ProtocolType.Tcp))
        {
            _IPAddress = IPAddress.Parse(IPString);
            _Port = Port;
        }

        public void ResetSocket()
        {
            this.Socket = new Socket(AddressFamily.InterNetwork,
                                      SocketType.Stream, ProtocolType.Tcp);
        }

        public async Task<bool> StartClient()
        {
            try
            {
                IPEndPoint RemoteEP = new IPEndPoint(_IPAddress, _Port);
                Socket.SendTimeout = 3000;
                Socket.ReceiveTimeout = 3000;
                await Socket.ConnectAsync(RemoteEP);
                return true;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
                return false;
            }
        }
    }

当TcpSocketClient实例化後 呼叫StartClient就可以对server进行连接

另外补个扩充方法 这就是刚刚讲说送出封包前 我需要在前面设置我需要接收多长的资料

例如我要送出 [1,2,3,4,5,6,7] 实际上会送出 [7,0,0,0,1,2,3,4,5,6,7]

而 7,0,0,0 就是指我後续要接收长度为7的资料

    public static class SocketExtension
    {
        public static byte[] GetDataWithHead(this byte[] datas)
        {
            var length = (uint)datas.Length;
            IEnumerable<byte[]> getDatas()
            {
                yield return BitConverter.GetBytes(length);//计算此次封包的长度 最长为uint.Maxvalue
                yield return datas;
            }
            return getDatas().SelectMany(x => x).ToArray();
        }
    }

下面是使用范例

//这是Server端
Task.Run(async () =>
{
    //设定Server的ip/port
    var server = new TcpSocketServer(IPAddress.Parse("127.0.0.1"), 2001);
    server.OnClientAccept += async (clientObj) =>//设定client接入的事件
    {
        //设定client如果传讯息来server server的处置方式
        //这里因为ProtocolComplete的关系 已经是完整封包 可以进行处理
        clientObj.OnDataReceive += async (datas) =>
        {
            /*
             * 此处如果比较懒 也可以用json来传物件
             */
            //这里就自己定义你的封包内容 我假设我client只有传一个long 这个long是时间ticks
            var datetime = new DateTime(BitConverter.ToInt64(datas));
            Console.WriteLine($"ServerWriteLine : {datetime}");//印出client传来的时间
            
            datetime = datetime.AddSeconds(20);//加个20秒 回传client
            await clientObj.SendAsync(BitConverter.GetBytes(datetime.Ticks));
        };

        await clientObj.StartReceiveAsync();//开始接收client来的讯息
    };
    await server.StartListening();//开始接受接入请求
});

//这是Client端 可以多复制几个启动多个

    _ = Task.Run(async () =>
    {
        //设定要连接的ip/port
        var client = new TcpSocketClient("127.0.0.1", 2001);
        //设定接收到的资料处理
        client.OnDataReceive += (datas) =>
        {
            var datetime = new DateTime(BitConverter.ToInt64(datas));
            //印出Server回传来的+20秒的时间
            Console.WriteLine($"ClientWriteLine : {datetime}");
        };
        //开始连接 会回传bool true才是连接成功
        //此处就不特别判断了
        await client.StartClient();
        //连接完成才会继续跑这里(成不成功自己再判断)
        //开始接收资料
        _ = client.StartReceiveAsync();
        
        while(true)
        {
            //送出讯息给Server
            //对Server送出当前时间 每五秒送一次
            await client.SendAsync(BitConverter.GetBytes(DateTime.Now.Ticks));
            await Task.Delay(TimeSpan.FromSeconds(5));
        }
    });


这样TCP/IP Server Client的核心功能就大致完成了

剩下的下一篇再继续!


<<:  纠正不合规问题并减轻风险, 您最关心的项目为何?

>>:  【系统程序】2.1基本组译器功能

Swift纯Code之旅 Day26. 「客制化Switch按钮」

前言 我们可以到Switch开启时背景色会是绿色,关掉时却是黑色,这样其实跟原本IPhone内建的也...

D17. 学习基础C、C++语言

D17. 题目练习 大小写转换 转大写 #include<stdio.h> #inclu...

【Day 4】DevOps x Containerized x 王大陆都知道的容器化好处

tags: 铁人赛 Docker Container Microservice DevOps 概述 ...

存放资料的 state、module

在 JavaScript 中,储存资料的方式,长这样。 { name: 'Chris', age: ...

Day28-让Xcode与模拟器并排显示在同画面

用文字说明可能有点模糊,我用图片举例说明,具体效果就像这张图片: 这样的效果在调试App时,有很大的...