最近都在全台跑面试
都没时间继续写..
刚好面试某金控
面试官出了个回家作业给我
就花了一个下午把它完成 顺便做为这次的主题
需求如下
写个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的核心功能就大致完成了
剩下的下一篇再继续!
前言 我们可以到Switch开启时背景色会是绿色,关掉时却是黑色,这样其实跟原本IPhone内建的也...
D17. 题目练习 大小写转换 转大写 #include<stdio.h> #inclu...
tags: 铁人赛 Docker Container Microservice DevOps 概述 ...
在 JavaScript 中,储存资料的方式,长这样。 { name: 'Chris', age: ...
用文字说明可能有点模糊,我用图片举例说明,具体效果就像这张图片: 这样的效果在调试App时,有很大的...