关于ClientWebSocket的异步转同步,我把这个项目搬出来,讲讲清楚

中文命名法 2018-03-02 11:04:14
加精
做的是跨平台套利,几十个平台,按照行情更新和交易指令传送方式可以分成3类:
一,行情接受使用WebSocket,交易通过WebSocket;
二,行情接受使用WebSocket,交易通过Rest;
三,行情接受使用Rest, 交易通过Rest;
每个平台我写了一个实例,集成一个的接口,方便行情排序和下单。所有行情更新功能,以及下单撤单功能都封装好了。现在要解决的就是统一调用的问题。
interface 平台接口
{
所有平台 平台 { get; } //枚举类型
Double[,] 余额 { get; }
Double[,] 委买 { get; set; }
Double[,] 委卖 { get; set; }
event Action<所有通知, 所有平台, Byte> 更新;
void 启动();
Task<Double> 计算成交量(交易方向 方向, Double 价格, Double 数量);
}
这几天讨论的焦点都围绕这个Task<Double> 计算成交量(交易方向 方向, Double 价格, Double 数量);
Rest模式下是通过HttpClient执行:发送订单,返回单号,撤销订单,返回单号,查询订单,返回成交量。已经封装好了。
WebStocket模式下,发送msg和接收msg是完全独立分开的,发送()是不能调用接收()的。我贴一段完整代码大家看一下:
struct okex : 平台接口
{
//此处省略100字
String 单号;
Double 成交量;
Task 延时任务;
public okex(Byte 位置, 所有代币[] 交易对)
{
//此处省略100字
单号 = String.Empty;
成交量 = 0;
延时任务 = null;
}
public async void 启动()
{
客户端 = new ClientWebSocket();
await 客户端.ConnectAsync(new Uri("wss://real.okex.com:10441/websocket"), CancellationToken.None);
监听();
订阅行情();
登陆();
账户信息();
定时器响应 = new Timer(检查状态, null, 29000, 30000);
}
void 检查状态(object 参数) => 发送("{'event':'ping'}");
async void 监听()
{
String 缓存 = String.Empty;
SByte 累计差 = 0;
const UInt16 最大字节数 = 4092; //超出后将拆分
var 缓冲 = new byte[最大字节数 * 2]; //服务端最大4092
while (客户端.State == WebSocketState.Open)
{
var 字节 = await 客户端.ReceiveAsync(new ArraySegment<byte>(缓冲), CancellationToken.None);
String 收到 = Encoding.Default.GetString(缓冲, 0, 字节.Count);
解析(Datetime.Now, 收到);
}
}
void 解析(DateTime 时间, String 消息)
{
if (消息 == "{\"event\":\"pong\"}") //服务器响应心跳包
心跳时间 = DateTime.Now;
else
{
foreach (JObject 成员 in JArray.Parse(消息)) //遍历消息组
{
if (成员["channel"].ToString() == 表达式行情深度 && 方法.时间戳转时间(成员["data"]["timestamp"].ToString()) > 行情时间) //更新行情
{
行情时间 = 方法.时间戳转时间(成员["data"]["timestamp"].ToString());
更新(所有通知.行情, 平台, 位置);
}
else if (成员["channel"].ToString() == 表达式成交通知) //交易通知
{
if ((SByte)成员["data"]["status"] == -1) //撤单后的成交明细
{
成交量 = (Double)成员["data"]["completedTradeAmount"];
//回复运行等待任务
}
}
else if (成员["channel"].ToString() == "ok_spot_order") //下单事件
单号 = 成员["data"]["order_id"].ToString();
else if (成员["channel"].ToString() == "ok_spot_cancel_order") //撤单事件
{}
else if (成员["channel"].ToString() == "login") //登陆事件
{}
else if (成员["channel"].ToString() == "ok_spot_userinfo") //查询余额
{
if ((bool)成员["data"]["result"])
更新(所有通知.余额, 平台, 位置);
}
}
}
}
public void 订阅行情()
{
发送($"{{'event':'addChannel','channel':'{表达式行情深度}'}}");
}
public void 登陆()
{
发送("{'event':'login','parameters':{'api_key':'" + 大 + "','sign':'" + 参数签名(new String[1], 0) + "'}}");
}
public void 账户信息()
{
发送("{'event':'addChannel','channel':'ok_spot_userinfo','parameters':{'api_key':'" + 大 + "','sign':'" + 参数签名(new String[1], 0) + "'}}");
}
void 撤单()
{
发送($"{{'event':'addChannel','channel':'ok_spot_cancel_order','parameters':{{'api_key':'{大}','sign':'{密文}','symbol':'{表达式交易对}','order_id':'{单号}'}}}}");
}
async void 发送(string 指令)
{
await 客户端.SendAsync(new ArraySegment<byte>(Encoding.Default.GetBytes(指令)), WebSocketMessageType.Text, true, new CancellationToken());
}
public async Task<double> 计算成交量(交易方向 方向, double 价格, double 数量)
{
发送($"{{'event':'addChannel','channel':'ok_spot_order','parameters':{{'api_key':'{大}','sign':'{密文}','symbol':'{表达式交易对}','type':'{ (方向 == 交易方向.买 ? "buy" : "sell")}','price':{价格},'amount':{数量}}}}}");
延时任务 = new Task(等待);
await new Task(等待); //卡死
return 成交量;
}
void 等待() => 延时任务.Wait();
}
WebStocket不是一问一答机制的,当程序运行起来后,服务器就不停想我发送msg了。为了可以被主程序调用,我要封装一个async Task<Double> 计算成交量(交易方向 方向, Double 价格, Double 数量);
我想要做到的功能是,向服务端发送一条msg,此刻没有返回值,休眠这个任务,但不能卡死主线程。
服务器向我发送的msg里会有一条和我发送出去的有相通id,它会被解析()这个功能里的if识别出来。这时把值放到该类的成交量字段里去,
然后恢复async Task<Double> 计算成交量这个任务,让它把成交量字段返回给主程序。
private void 测试按钮_Click(object sender, EventArgs e)
{
okex 币行 = new okex(参数);
消息框.Text = 币行.测试任务().Result.ToString();
}
程序卡死在代码的第97行上,这行上有await,搞了那么多天了,把内容说清楚,讲明白。
...全文
6416 26 打赏 收藏 转发到动态 举报
写回复
用AI写文章
26 条回复
切换为时间正序
请发表友善的回复…
发表回复
中文命名法 2018-04-18
  • 打赏
  • 举报
回复
引用 25 楼 zanfeng 的回复:
这个东西要是能研究出来那就牛逼了。不过现在都是搓和成交。亏了也是有可能的。
为什么不能做出来呢?其实就是当价差条件满足后,谁的速度快了。而速度除了网速和硬件,就取决于程序的效率了。
足球中国 2018-04-09
  • 打赏
  • 举报
回复
这个东西要是能研究出来那就牛逼了。不过现在都是搓和成交。亏了也是有可能的。
xuzuning 2018-03-19
  • 打赏
  • 举报
回复
正规的通讯狭义的报文中,都有报头和指示数据长度的字段,所以很容易识别边界。何况 http 协议才去的是一问一答的形式,不可能出现两条消息一起来的情况
中文命名法 2018-03-19
  • 打赏
  • 举报
回复
引用 12 楼 xuzuning 的回复:
你把事情弄反了!WebStocket 返回的是一个序列,并且你不能确切的知道序列中的下一个元素什么时候到达。这是典型的异步工作方式 而 REST 是一问一答的同步工作方式。所以,要统一管理这两种方式,要将 REST 模拟成异步方式。
之前的回复我都学习到了,现在想继续学习rest转异步。查了很多资料,了解到http访问是基于Socket的。这个机制与我别的类使用的WebStocket又很接近,看上去可以统一管理。有个问题就是,Socket客户端去接受服务器过来的字节时,如何区分2条消息的边界?我访问的服务器给我的head里面没有Lenth
qq_34407634 2018-03-07
  • 打赏
  • 举报
回复
66666666 写很不错,收藏了
中文命名法 2018-03-05
  • 打赏
  • 举报
回复
引用 1 楼 sp1234 的回复:
随便写个 async/await 的例子吧。虽然代码不精简,但是起码这是异步的意思
谢谢大牛,办法很好,不占线程。已经消化吸收。
public partial class 测试 : Form
{
    public 测试() => InitializeComponent();
    static String 成交量 = "已算出";
    Task<String> 回调 = new Task<String>(() => 成交量);
    async void 显示() => label1.Text = await 回调;
    private void button1_Click(object sender, EventArgs e) => 显示();
    private void button2_Click(object sender, EventArgs e) => 回调.Start();
}
  • 打赏
  • 举报
回复
嗯,在专门的通讯程序设计中,使用纯粹异步回调方式来编写简洁的代码,而不是用 ManualResetEvent 来阻塞线程,是有原因的。因为讨论往往还只是在编程模式阶段,没有拿出数据来测试,所以往往说明不清。 比如说,发送信息往往是并发的,而服务器也往往并发接入数万客户端会话,特别是许多稍微高级一点点的内部对象交互通讯系统都是事件驱动的,所以不使用阻塞方式来实现异步操作这是一个需要先学会的知识。只是偶尔在随便地举个例子的时候才写阻塞代码。
raynors 2018-03-04
  • 打赏
  • 举报
回复
那天是在半夜回的贴,没想到讨论还挺激烈, 某位大哥基本功很扎实,代码很专业。 1# 代码跟我 8# 代码都是这问题的解决方案,都是建立 线程(过程)监控列表,独立任务通过注册到“”监控列表“”从而实现异步等待。
  • 打赏
  • 举报
回复
如果你学过 async/await 语法,应该学会一眼就看出代码
    public async Task<double> 计算成交量(交易方向 方向, double 价格, double 数量)
    {
        发送($"{{'event':'addChannel','channel':'ok_spot_order','parameters':{{'api_key':'{大}','sign':'{密文}','symbol':'{表达式交易对}','type':'{ (方向 == 交易方向.买 ? "buy" : "sell")}','price':{价格},'amount':{数量}}}}}");
        延时任务 = new Task(等待);
        await new Task(等待);    //卡死
        return 成交量;
    }
这里的“return 成交量”纯粹是一个胡乱拼凑来的“全局变量”概念,而不是 async/await 机制。 真正的 async/await 代码是形如这样的代码
    public async Task<double> 计算成交量(交易方向 方向, double 价格, double 数量)
    {
        var id = 获得消息唯一编号();
        发送($"{{'event':'addChannel','channel':'ok_spot_order','parameters':{{'api_key':'{大}','sign':'{密文}','symbol':'{表达式交易对}','type':'{ (方向 == 交易方向.买 ? "buy" : "sell")}','price':{价格},'amount':{数量}}}}}", id);
        var result = await 消息返回结果(id); 
        return result;
    }
这种代码。这种代码就算是如你问题标题所说“模仿同步代码”,它也是学过语法形式才有根有据地写出来的代码。假设你用一个无根据的“成交量”变量来作为返回,那肯定要整天在那里纠结了。
raynors 2018-03-03
  • 打赏
  • 举报
回复
class 我的信号灯类 { public 我的信号灯类(int n ,AutoResetEvent e ) { n信号灯识别标志 = n; m信号灯对象 = e; } public int n信号灯识别标志; public AutoResetEvent m信号灯对象; } List<我的信号灯类> 信号灯list = new List<我的信号灯类>(); private void 注册增加监控项目(int flag, AutoResetEvent e) { 信号灯list.Add(new 我的信号灯类(flag, e)); } private void 查找并激活监控项目(int flag) { try { 信号灯list.Find(s => s.n信号灯识别标志 == flag).m信号灯对象.Set(); } catch { //.... } } private void 解析(DateTime 时间, String 消息) { foreach (JObject 成员 in JArray.Parse(消息)) //遍历消息组 { if (属于反馈类信息标记) //更新行情 { int refflag = 消息.flag; 查找并激活监控项目(refflag); } } } private void 计算成交量() { int refflag = 唯一标记; AutoResetEvent 信号灯; 注册增加监控项目(唯一标记, 信号灯) 发送($"{{'event':'addChannel','channel':'ok_spot_order','parameters':{{'api_key':'{大}','sign':'{密文}','symbol':'{表达式交易对}','type':'{ (方向 == 交易方向.买 ? "buy" : "sell")}','price':{价格},'amount':{数量}}}}}"); 信号灯.WaitOne(); 正儿八经的计算过程(); }
  • 打赏
  • 举报
回复
要注意,async/await 根本不是信号灯阻塞机制。如果有这个想法,那么应该重新学习 async/await 的技术文章。
  • 打赏
  • 举报
回复
你仔细单步调试一下代码,看看那些语句分别是以什么次序的。 在
            return await session.callback;
这条语句执行之前,其实没有任何线程在那里死等。如果纠结什么“阻塞、waitone、(你的)等待”,这就太不实际了。根本没有那些多余的代码!你可以通过调试看到,当
lst[0].callback.Start();
被执行之后立刻触发了
return await session.callback;
代码,这里的关键是异步回调的基本概念。 这就好像是把用头走路改为用脚走路,或者犹如分清楚镜子里的你和真实的自己,你的程序之所以“死掉”是因为走火入魔地完全用同步顺序执行的脑袋来理解异步设计,而且无法开窍。
raynors 2018-03-03
  • 打赏
  • 举报
回复
其实我很了解你的需求和意思,但希望你把以前那个贴给结了最好。 你就是想 客户端仍然是一问一答的模式,却又不想网络传输是一问一答。 其实思路很好理解,你SOCKET端口(对象)实际上只有一个,BUFF缓冲区也实质上只有一个。 思路1: 客户端问问题,执行到阻塞,实际上就是看做一个线程挂起了,如果同时有N个问题,那么就是N个线程,你不可能在每个线程内都去注册 异步接受SOCKET的工作,因为之前说了,SOCKET端口(对象)实际上只有一个,N个同时去注册某个过程,实质上只会最后一个生效。 思路2: 线程阻塞 -通知-回复执行 主要是用到 AutoResetEvent 之前那个帖子29# 那个信号灯的例子。 没有别的办法。 思路3,因为是多并发处理 信号灯不能是一个公共对象,而是要作为过程内定义的对象,然后把这个对象作为参数传递给一个公共列表。 在异步接受消息过程的时候,首先判断消息反馈的标记,通过标记在列表内找到 信号灯对象,然后执行 信号灯.SET()
sp1234_maJia 2018-03-03
  • 打赏
  • 举报
回复
我给你举了两个例子,分别用时髦的 async/await 和经典的回调委托两种形式,来说明异步过程是个什么概念。 如果你还是满脑子“用同步处理的脑袋来硬要说成是异步处理”,那么我只能说你不能设计异步过程程序,而对于这个顽固性并没有什么解决办法。
  • 打赏
  • 举报
回复
当 sendmessage 之后,实际上发送工作就结束了。 当没有消息来的时候,根本不占用任何线程。当有信息到来事件发生,才会瞬间有一个回调动作。 这中间没有任何线程在那里死等着,不要阻塞你自己的思路。
  • 打赏
  • 举报
回复
当发送信息时,把将来回调注册到一个 lst 列表中。然后当收到信息之后,找到对应的客户端、对应收到信息的序号的那个回调,然后使用
s.Result=.....;
s.callback.Start()
或者
s.callback(....)
这样的方法回调。 发送信息时可以发送很多命令,收到返回信息的顺序跟发送命令完全可能不同。 这里如果抱着死板地“等待、阻塞、顺序处理”的思路来理解异步,那就只能靠悟性了。
秋的红果实 2018-03-03
  • 打赏
  • 举报
回复
这几天忙,有时间好好看看 请问lz,监听()必须要放到启动()里面而且不断循环吗?
xuzuning 2018-03-03
  • 打赏
  • 举报
回复
你把事情弄反了! WebStocket 返回的是一个序列,并且你不能确切的知道序列中的下一个元素什么时候到达。这是典型的异步工作方式 而 REST 是一问一答的同步工作方式 所以,要统一管理这两种方式,要将 REST 模拟成异步方式,而不是将 WebStocket 模拟成同步方式(因为是不现实的) async/await 可以将异步方式用同步方式写出,但并没有改变异步工作的事实 只是将异步回调方法与异步调用写在同一代码段中 ,看上去像同步一样: 异步调用(回调方法); await 异步调用(); 回调方法体; 而将 REST 模拟成异步只需: var value = REST调用(); 回调方法(value);
ljgmz 2018-03-03
  • 打赏
  • 举报
回复
学习了
引用 15 楼 sp1234 的回复:
当看到
        static async Task<string> SendMessage(byte[] message)
        {
            var session = new Session
            {
                //......
            };
            session.callback = new Task<string>(() => session.Result);
            lst.Add(session);
            sendmessage(message);
            return await session.callback;
 
        }
这样的代码时,应该清楚地能够说出,await 之前的代码是这个方法主体要执行的代码,而 await 后边(右边和下边)的代码是封装到 Task<string> 类型对象里边去的委托,并不会立刻被执行。 切不可把 await 理解为什么阻塞、信号量之类的概念。
  • 打赏
  • 举报
回复
当看到
        static async Task<string> SendMessage(byte[] message)
        {
            var session = new Session
            {
                //......
            };
            session.callback = new Task<string>(() => session.Result);
            lst.Add(session);
            sendmessage(message);
            return await session.callback;
 
        }
这样的代码时,应该清楚地能够说出,await 之前的代码是这个方法主体要执行的代码,而 await 后边(右边和下边)的代码是封装到 Task<string> 类型对象里边去的委托,并不会立刻被执行。 切不可把 await 理解为什么阻塞、信号量之类的概念。
加载更多回复(6)

110,500

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧