基于webapi的websocket聊天室(番外二)

webapi,websocket · 浏览次数 : 0

小编点评

```csharp using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; public class HttpRequet { /// <summary> /// 解析HTTP消息 /// </summary> public HttpRequet(string str) { Str = str; //开始行 var startLine = str.Split('\r\n')[0]; var lines = startLine.Split('\r\n'); httpMethod = lines[0].Split(' ')[0]; path = lines[0].Split(' ')[1]; //头部 var headersLines = str.Split('\r\r\n')[0].Split('\r\n'); headers = new Dictionary(); for (int i = 1; i < headersLines.Length; i++) { var header = headersLines[i].Split(': '); headers.Add(header[0], header[1]); } } /// <summary> /// 请求原始消息 /// </summary> public string Str { get; } /// <summary> /// 请求方法 /// </summary> public string httpMethod { get; internal set; } /// <summary> /// 请求路径 /// </summary> public string path { get; set; } /// <summary> /// 头部字段 /// </summary> public Dictionary headers { get; set; } /// <summary> /// 判断是否是转协议的请求 /// </summary> /// <returns></returns> public bool IsWebsocket() { if (headers.ContainsKey("Connection") & headers["Connection"] == "Upgrade" & headers.ContainsKey("Upgrade")) return true; else return false; } /// <summary> /// 响应转协议请求并未用当前连接创建一个WebSocket对象 /// </summary> /// <param name="client"></param> /// <returns></returns> public async Task AcceptWebsocket(TcpClient client, string Sec_WebSocket_Key) { using (MemoryStream memoryStream = new MemoryStream()) { string header = @"\HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: {GenerateResponseKey(Sec_WebSocket_Key)}\"; memoryStream.Write(new byte[] { 0x3C, 0x3A, 0x41, 0x82, 0x64, 0x74, 0x65, 0x6C, 0x6F, 0x75, 0x6E, 0x79, 0x67, 0x65 }); await client.Client.SendAsync(new byte[] { 0x47, 0x65, 0x64, 0x6F, 0x6D, 0x65 }, 0, 0); Console.WriteLine(header); return WebSocket.CreateFromStream(client.GetStream(), true, null, TimeSpan.FromSeconds(10)); } } public static string GenerateResponseKey(string requestKey) { const string guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; string concatenated = requestKey + guid; byte[] hashed = System.Security.Cryptography.SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(concatenated)); return Convert.ToBase64String(hashed); } } ```

正文

我比较好奇的是webapi服务器怎么处理http请求和websocket请求。有了上一篇番外的研究,这里就可以试着自己写个非常简易的webapi服务器来接收这两种请求。

效果

  • http请求
    消息打印
    image
    响应解析
    image

  • websocket请求
    消息打印
    image
    使用聊天室测试
    image

其实两种请求差不多,就只是一些头部字段有差别

  • http消息

    //客户端发送的消息
    string clientMsg = @"Get /httppath?msg=你好 HTTP/1.1
    CustomField:f1
    CustomField2:f2
    ";
    
    //服务端发送的消息
    string serverMsg = @"HTTP/1.1 200
    CustomField2:f2
    
    数据以收到";
    
  • websocket消息

    //客户端发送的消息
    string clientMsg = @"Get /httppath HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: xxxxx
    ";
    
    //服务端发送的消息
    string serverMsg = @"HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: xxxxx
    ";
    

伪代码分析

http头部是ASCII编码。body部分默认也是,除非指定了content-type
http因为是无连接的,所以请求处理过程应该是这样

  1. 客户端解析clientMsg获取要连接的服务器
  2. 客户端根据请求先建立tcp连接
  3. 客户端发送ASCII.GetBytes("Get /httppath....")
  4. 服务端接收后GetString(clientMsg)
  5. 服务端根据请求路径执行对应方法Action()
  6. 服务端发送ASCII.GetBytes("HTTP/1.1 200....")
  7. 服务端关闭连接

websocket发送的第一条消息也是采用http格式,流程相似,但是要保持连接,所以请求处理过程有所差异

  1. 客户端解析clientMsg获取要连接的服务器
  2. 客户端根据请求先建立tcp连接
  3. 客户端发送GetBytes("Get /httppath...."),然后,调用等待消息发方法阻塞线程awite ReciveAsync()
  4. 服务端接收后GetString(clientMsg)
  5. 服务端看到消息头部包含三个字段Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: xxx,开一个接收消息的线程
  6. 服务端发送GetBytes("HTTP/1.1 101...")
  7. 服务端在接收消息的线程中写个while循环,判断监听客户端发来的消息,并调用对应方法处理
  8. 客户端收到消息后判断是101消息,开一个接收消息的线程
  9. 客户端在接收消息的线程中写个while循环,判断监听服务端发来的消息,并调用对应方法处理

写一个 HTTP & WebSocket 服务器和客户端

  • 首先是解析消息
    var buffer = new byte[1024*4];
    int msgLength = await client.Client.ReceiveAsync(new ArraySegment<byte>(buffer));
    string str=UTF8Encoding.UTF8.GetString(buffer,0,msgLength);
    Console.WriteLine(str);
    HttpRequet request = new HttpRequet(str);
    
  • 核心思想是判断消息是不是符合websocket连接请求格式,而这非常容易
    public bool IsWebsocket()
    {
    	if (this.headers.ContainsKey("Connection") && this.headers["Connection"] == "Upgrade" 
    	&& this.headers.ContainsKey("Upgrade") && this.headers["Upgrade"] == "websocket")
    		return true;
    	else
    		return false;
    }
    
  • 然后是根据消息判断如何处理
    //转websocket的消息
    if (request.IsWebsocket())
    {
    	//用tcp连接构造一个WebSocket对象
    	WebSocket webSocket =await request.AcceptWebsocket(client, request.headers["Sec-WebSocket-Key"]);
    }
    //其他HTTP消息
    else
    {
    	string header = @$"HTTP/1.1 200
    CustomField2: f2
    content-type: text/html; charset=utf-8
    
    ";
    	string body = "数据以收到";
    }
    

完整代码

TCP与Socket端口测试.cs
    internal class Program
    {
        static void Main(string[] args)
        {
            //服务器
            if (args.Length == 1) {
                StartServer(args[0]);
            }
        }

        private static void StartServer(string args)
        {

            int serverPort = Convert.ToInt32(args);
            var server = new TcpListener(IPAddress.Parse("127.0.0.1"), serverPort);
            Console.WriteLine($"TCP服务器  127.0.0.1:{serverPort}");
            server.Start();
            int cnt = 0;
            Task.Run(async () =>
            {
                List<TcpClient> clients = new List<TcpClient>();
                while (true)
                {
                    TcpClient client = await server.AcceptTcpClientAsync();
                    clients.Add(client);
                    cnt++;
                    var ep = client.Client.RemoteEndPoint as IPEndPoint;
                    Console.WriteLine($"TCP客户端_{cnt}  {ep.Address}:{ep.Port}");
                    //给这个客户端开一个聊天线程
                    //操作系统将会根据游客端口对应表将控制权交给对应游客线程
                    StartChat(client);
                }
            }).Wait();
        }

        public static async Task StartChat(TcpClient client)
        {
            var buffer = new byte[1024*4];
            int msgLength = await client.Client.ReceiveAsync(new ArraySegment<byte>(buffer));
            string str=UTF8Encoding.UTF8.GetString(buffer,0,msgLength);
            Console.WriteLine(str);
            HttpRequet request = new HttpRequet(str);
            //转websocket的消息
            if (request.IsWebsocket())
            {
                WebSocket webSocket =await request.AcceptWebsocket(client, request.headers["Sec-WebSocket-Key"]);
                //发送一条websocket格式的打招呼消息
                var msg = new byte[] {
                        0x15,
                        0xe6,0x98,0x9f,0xe7,0xa9,0xb9,0xe9,0x93,0x81,0xe9,0x81,0x93,0xe5,0xa4,0xa7,0xe5,0xae,0xb6,0xe5,0xba,0xad,
                        0x00,
                        0x15,0x00,0x00,0x00,
                        0xe6,0xac,0xa2,0xe8,0xbf,0x8e,0xe8,0xbf,0x9b,0xe5,0x85,0xa5,0xe8,0x81,0x8a,0xe5,0xa4,0xa9,0xe5,0xae,0xa4
                    };
                await webSocket.SendAsync(msg, WebSocketMessageType.Binary, true, CancellationToken.None);
                //之后采用websocket规定的格式传输消息
                while (!webSocket.CloseStatus.HasValue)
                {
                    await webSocket.ReceiveAsync(buffer,CancellationToken.None);
                }
            }
            //其他HTTP消息
            else
            {
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    string header = @$"HTTP/1.1 200
CustomField2: f2
content-type: text/html; charset=utf-8

";
                    string body = "数据以收到";
                    //响应请求
                    memoryStream.Write(new ArraySegment<byte>(ASCIIEncoding.ASCII.GetBytes(header)));
                    memoryStream.Write(new ArraySegment<byte>(UTF8Encoding.UTF8.GetBytes(body)));
                    await client.Client.SendAsync(new ArraySegment<byte>(memoryStream.ToArray()));
                    Console.WriteLine(header+body);
                    //关闭连接
                    client.Close();
                }
            }
        }
    }

    public class HttpRequet
    {
        /// <summary>
        /// 解析HTTP消息
        /// </summary>
        public HttpRequet(string str)
        {
            Str = str;
            //开始行
            var startLine = str.Split("\r\n")[0];
            var lines= startLine.Split("\r\n");
            httpMethod = lines[0].Split(' ')[0];
            path = lines[0].Split(' ')[1];
            //头部
            var headerslines= str.Split("\r\n\r\n")[0].Split("\r\n");
            headers = new Dictionary<string, string>();
            for (int i = 1; i < headerslines.Length; i++)
            {
                var header = headerslines[i].Split(": ");
                headers.Add(header[0], header[1]);
            }
        }

        /// <summary>
        /// 请求原始消息
        /// </summary>
        public string Str { get; }
        /// <summary>
        /// 请求方法
        /// </summary>
        public string httpMethod { get; internal set; }
        /// <summary>
        /// 请求路径
        /// </summary>
        public string path { get; set; }
        /// <summary>
        /// 头部字段
        /// </summary>
        public Dictionary<string,string> headers { get; set; }

        /// <summary>
        /// 判断是否是转协议的请求
        /// </summary>
        /// <returns></returns>
        public bool IsWebsocket()
        {
            if (this.headers.ContainsKey("Connection") && this.headers["Connection"] == "Upgrade" && this.headers.ContainsKey("Upgrade") && this.headers["Upgrade"] == "websocket")
                return true;
            else
                return false;
        }

        /// <summary>
        /// 响应转协议请求并未用当前连接创建一个WebSocket对象
        /// </summary>
        /// <param name="client"></param>
        /// <returns></returns>
        public async Task<WebSocket> AcceptWebsocket(TcpClient client,string Sec_WebSocket_Key)
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                string header = @$"HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: {GenerateResponseKey(Sec_WebSocket_Key)}

";
                memoryStream.Write(new ArraySegment<byte>(ASCIIEncoding.ASCII.GetBytes(header)));
                await client.Client.SendAsync(new ArraySegment<byte>(memoryStream.ToArray()));
                Console.WriteLine(header);

                return WebSocket.CreateFromStream(client.GetStream(), true, null, TimeSpan.FromSeconds(10));
            }
        }

        public static string GenerateResponseKey(string requestKey)
        {
            const string guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
            string concatenated = requestKey + guid;
            byte[] hashed = System.Security.Cryptography.SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(concatenated));
            return Convert.ToBase64String(hashed);
        }
    }

与基于webapi的websocket聊天室(番外二)相似的内容:

基于webapi的websocket聊天室(番外二)

我比较好奇的是webapi服务器怎么处理http请求和websocket请求。有了上一篇番外的研究,这里就可以试着自己写个非常简易的webapi服务器来接收这两种请求。 效果 http请求 消息打印 响应解析 websocket请求 消息打印 使用聊天室测试 其实两种请求差不多,就只是一些头部字段有

基于webapi的websocket聊天室(番外一)

上一篇我已经实现了聊天室,并且在协议中实现了4种类型的消息传输。其实还可以添加video,audio,live等等类型。 不过假如把目前的协议看作RCP1.0版的话,这个版本就只支持有限的4种消息。精力有限啊。也许RCP2.0就可以把video,audio类型加进去? 这不是这篇番外考虑的。而是我在

基于webapi的websocket聊天室(四)

上一篇实现了多聊天室。这一片要继续改进的是实现收发文件,以及图片显示。 效果 问题 websocket本身就是二进制传输。文件刚好也是二进制存储的。 文件本身的传输问题不太,但是需要传输文件元数据,比如文件名和扩展名之类的。这很必要,如果我们想知道怎么展示这个文件的话。比如这个文件是图片还是word

利用云服务提供商的免费证书,在服务器上发布https前端应用和WebAPI的应用

我们如果要在服务器上发布https前端应用和WebAPI的应用,那么我们就需要用到https证书了。我们一般发布的应用的云服务器上,都会提供一定量的相关的免费证书(一般为20个)供我们使用,每个一年期限,到期再续即可,一般情况下基本上满足要求了,本篇随笔介绍如何基于云服务提供商的免费证书,在服务器上发布Nginx的前端应用和基于IIS的Web API接口的https应用处理。

基于 ActionFilters 的限流库DotNetRateLimiter使用

前言 在构建API项目时,有时出于安全考虑,防止访问用户恶意攻击,希望限制此用户ip地址的请求次数,减轻拒绝服务攻击可能性,也称作限流。接下来,我们就来学习开源库DotNetRateLimiter 如何轻松实现限流。 项目使用配置 安装Nuget包 在新建立的WebAPI项目中,通过Nuget包管理

基于.NetCore开发博客项目 StarBlog - (22) 开发博客文章相关接口

## 前言 本文介绍博客文章相关接口的开发,作为接口开发介绍的第一篇,会写得比较详细,以抛砖引玉,后面的其他接口就粗略带过了,着重于WebApi开发的周边设施。 涉及到的接口:文章CRUD、置顶文章、推荐文章等。 开始前先介绍下AspNetCore框架的基础概念,MVC模式(前后端不分离)、WebA

EDP .Net开发框架--WebApi

EDP是一套集组织架构,权限框架【功能权限,操作权限,数据访问权限,WebApi权限】,自动化日志,动态Interface,WebApi管理等基础功能于一体的,基于.net的企业应用开发框架。通过友好的编码方式实现数据行、列权限的管控。

EDP .Net开发框架--组织架构

EDP是一套集组织架构,权限框架【功能权限,操作权限,数据访问权限,WebApi权限】,自动化日志,动态Interface,WebApi管理等基础功能于一体的,基于.net的企业应用开发框架。通过友好的编码方式实现数据行、列权限的管控。

EDP .Net开发框架--自动化日志

EDP是一套集组织架构,权限框架【功能权限,操作权限,数据访问权限,WebApi权限】,自动化日志,动态Interface,WebApi管理等基础功能于一体的,基于.net的企业应用开发框架。通过友好的编码方式实现数据行、列权限的管控。

EDP .Net开发框架--权限

EDP是一套集组织架构,权限框架【功能权限,操作权限,数据访问权限,WebApi权限】,自动化日志,动态Interface,WebApi管理等基础功能于一体的,基于.net的企业应用开发框架。通过友好的编码方式实现数据行、列权限的管控。