WebSocket

发布时间 2023-08-14 10:51:56作者: 朦朦胧胧的月亮最美好

在搭建聊天室时,选择使用TCP请求而不是HTTP请求是因为TCP(传输控制协议)和HTTP(超文本传输协议)具有不同的特性,适用于不同的场景。以下是选择TCP请求而不是HTTP请求的一些原因:

  1. 即时性:

    TCP连接可以保持长时间,使得聊天室能够实时地传输消息,而不需要每次都建立新的连接,从而减少了延迟。

    HTTP协议是无状态的,每次请求都需要建立新的连接,不如TCP适合实时聊天应用。

  2. 双向通信:

    TCP支持全双工通信,客户端和服务器可以同时发送和接收数据

    HTTP请求通常是单向的,客户端发起请求,服务器响应,无法保持持续的双向通信。

  3. 自定义协议:

    TCP提供更大的灵活性,允许你定义自己的数据格式和通信规则

    HTTP协议的数据格式和操作有一定的限制。

SignalR

using Microsoft.AspNetCore.SignalR;

namespace WebApplication7.Hubs
{
    public class ChatHub : Hub
    {
        public async Task Send(string name, string message)
        {
            await Clients.All.SendAsync("broadcastMessage", name, message);
        }

        public void SendTest(string name, string message)
        {
            Console.WriteLine("name:"+name+",messae"+message);
        }
    }
}
var connection = new signalR.HubConnectionBuilder().withUrl('/chat').build();
connection.on('broadcastMessage', function (name, message) {});
document.getElementById('sendmessage').addEventListener('click', function (event) {
	connection.invoke('send', name, messageInput.value);
	connection.invoke('sendtest', name, messageInput.value);
});
connection.start().then(function () {console.log('connection started');})
.catch(error => {
	console.error(error.message);
});
builder.Services.AddSignalR();
app.UseEndpoints(endpoints =>
{
    endpoints.MapHub<ChatHub>("/chat");
});

WebSocket

特点:

  1. 全双工通信: WebSocket支持在同一连接上同时进行双向通信,服务器可以主动向客户端推送数据,而不需要客户端显式地发起请求。
  2. 持久连接: 与HTTP请求-响应不同,WebSocket连接是持久的,一旦建立,可以保持活动状态,允许在任何时间点进行数据传输。
  3. 低延迟: 由于不需要频繁地建立和关闭连接,WebSocket通信通常具有低延迟,适用于实时应用场景。
  4. 协议支持: WebSocket协议是标准化的,支持多种编程语言和平台,使不同系统之间的通信更加方便。
using System.Net.WebSockets;
using Microsoft.AspNetCore.Mvc;

namespace WebSocketsSample.Controllers;

#region snippet_Controller_Connect
public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    #endregion
    
    private static async Task Echo(WebSocket webSocket)
    {
        var buffer = new byte[1024 * 4];
        var receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);

        while (!receiveResult.CloseStatus.HasValue)
        {
            await webSocket.SendAsync(
                new ArraySegment<byte>(buffer, 0, receiveResult.Count),
                receiveResult.MessageType,
                receiveResult.EndOfMessage,
                CancellationToken.None);

            receiveResult = await webSocket.ReceiveAsync(
                new ArraySegment<byte>(buffer), CancellationToken.None);
        }

        await webSocket.CloseAsync(
            receiveResult.CloseStatus.Value,
            receiveResult.CloseStatusDescription,
            CancellationToken.None);
    }
}
app.UseWebSockets();
socket = new WebSocket(connectionUrl.value);
socket.onopen = function (event) {};
socket.onclose = function (event) {};
socket.onerror = function (event) {};
socket.onmessage = function (event) {// user event.data};
socket.send("Hello, Server!");

TCP

import socket

# 创建一个TCP/IP套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定地址和端口
server_address = ('localhost', 8888)
server_socket.bind(server_address)

# 开始监听
server_socket.listen(5)  # 最多允许5个等待连接的客户端

print("等待客户端连接...")

while True:
    # 等待客户端连接
    client_socket, client_address = server_socket.accept()
    
    print(f"与客户端 {client_address} 建立连接")
    
    # 接收数据
    data = client_socket.recv(1024)
    print(f"接收到的数据:{data.decode('utf-8')}")
    
    # 发送响应
    response = "Hello, client! This is the server."
    client_socket.send(response.encode('utf-8'))
    
    # 关闭连接
    client_socket.close()
import socket

# 创建一个TCP/IP套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接服务器
server_address = ('localhost', 8888)
client_socket.connect(server_address)

# 发送数据
message = "Hello, server! This is the client."
client_socket.send(message.encode('utf-8'))

# 接收响应
response = client_socket.recv(1024)
print(f"服务器的响应:{response.decode('utf-8')}")

# 关闭连接
client_socket.close()

TCP(传输控制协议)是一种用于在计算机网络中进行可靠数据传输的协议。在讲解TCP建立的过程时,涉及到一些关键的概念,包括队列、bind、listen、accept和backlog。以下是TCP建立连接的基本过程及相关概念的解释:

  1. 队列(Queue): 在服务器端,当多个客户端请求连接时,服务器需要将这些连接请求存放在一个等待队列中,以便逐一进行处理。这个队列称为“连接请求队列”或“SYN队列”。
  2. Bind: 在服务器端创建一个套接字(socket)时,需要将该套接字绑定到一个特定的IP地址和端口号上。这个过程称为“绑定(bind)”。
  3. Listen: 一旦套接字被绑定到特定的IP地址和端口号,服务器就可以开始监听传入的连接请求。通过调用listen()函数,服务器将套接字置于监听状态,等待客户端的连接请求。
  4. Accept: 当服务器处于监听状态并接收到客户端的连接请求时,服务器将调用accept()函数来接受这个连接。accept()函数会返回一个新的套接字,用于在服务器和客户端之间进行通信。这个新的套接字是服务器用于与该特定客户端之间进行数据交换的通道。
  5. Backlog: listen()函数的参数中通常包含一个backlog参数,表示连接请求队列(accept队列)的最大长度。当队列已满时,新的连接请求将被拒绝。backlog参数决定了服务器可以接受的同时连接请求数量。

TCP连接的建立过程如下:

  1. 服务器端: 创建套接字(socket())。绑定套接字到特定IP地址和端口号(bind())。开始监听传入的连接请求(listen())。进入循环,不断接受连接请求(accept()),处理客户端请求。
  2. 客户端: 创建套接字(socket())。 向服务器端发起连接请求(connect())。等待服务器端的响应。 如果服务器接受连接,则连接建立成功,可以进行数据传输。
  • SYN队列:在第一次握手期间,服务器将接收到的客户端连接请求(SYN数据包)放入SYN队列中。
  • accept队列:在第三次握手期间,一旦服务器发送完SYN和ACK数据包,等待客户端的ACK确认。当服务器收到客户端的确认后,它将客户端的连接从SYN队列中移入accept队列,此时连接已经建立,服务器可以与客户端进行数据传输。

UDP

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('localhost', 8888)
server_socket.bind(server_address)

while True:
    data, client_address = server_socket.recvfrom(1024)
    print(f"Received data from {client_address}: {data.decode('utf-8')}")
import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('localhost', 8888)

message = "Hello, server! This is a UDP message."
client_socket.sendto(message.encode('utf-8'), server_address)

TCP协议首部:

TCP首部包含了许多字段,用于控制和管理数据的传输,确保数据的可靠性和顺序性。

  1. 源端口(Source Port)和目标端口(Destination Port): 16位字段,分别指示源和目标应用程序的端口号。
  2. 序号(Sequence Number): 32位字段,表示发送方的数据字节在整个连接中的序号。用于保证数据的有序性。
  3. 确认号(Acknowledgment Number): 32位字段,在确认报文中,指示期望收到的下一个数据字节的序号。
  4. 数据偏移(Data Offset): 4位字段,指示TCP首部的长度,以4字节为单位。TCP首部长度为20到60字节,因此数据偏移的值为5到15。
  5. 标志位(Flags): 6位字段,包含了控制连接状态和数据传输的标志,如ACK、SYN、FIN等。
  6. 窗口大小(Window Size): 16位字段,指示接收方的可用缓冲区大小,用于流量控制。
  7. 校验和(Checksum): 16位字段,用于检测TCP首部和数据在传输过程中的错误。
  8. 紧急指针(Urgent Pointer): 16位字段,只在URG标志位设置时有效,用于指示紧急数据的位置。
  9. 选项(Options): 可变长度字段,包含了一些附加的控制选项,如最大报文段长度、时间戳等。

UDP协议首部:

UDP首部相对简单,仅包含了几个基本字段。

  1. 源端口(Source Port)和目标端口(Destination Port): 16位字段,分别指示源和目标应用程序的端口号。
  2. 长度(Length): 16位字段,指示UDP首部和数据的总长度,以字节为单位。
  3. 校验和(Checksum): 16位字段,用于检测UDP首部和数据在传输过程中的错误。在UDP协议中,校验和是可选的,可以被设置为0表示不使用。

UDP协议相对于TCP协议更为简单,适用于不需要可靠性和顺序性保证的数据传输场景,如音频、视频流等。而TCP协议提供了更多的控制和管理机制,确保了数据的可靠传输和有序性。

组播

想象一下,您是一家繁忙的披萨店的店主,而店员是处理连接的线程。披萨店里有很多桌子,每个桌子都坐着不同的顾客,每位顾客都点了不同种类的披萨。您是店主,需要确保每位顾客都得到及时的服务。

现在,您可以有两种处理方式:

  1. 每桌一店员(传统多线程):您雇佣了很多店员,每个店员负责一张桌子上的顾客。这样可以确保每位顾客得到专门的服务,但随着顾客数量的增加,您需要雇佣更多的店员,管理变得复杂。
  2. 一个店员处理多桌(I/O 多路复用):您雇佣了一个聪明的店员,这个店员可以同时观察多张桌子上的顾客。他可以注意到哪些桌子上的顾客需要点菜,哪些桌子上的顾客已经吃完了。这个店员非常高效,可以在不浪费时间的情况下为每个桌子上的顾客提供服务。

在这个比喻中,每张桌子就代表一个连接,每位顾客就代表连接上的事件(如数据到达)。传统多线程就像为每个桌子雇佣一个店员,而 I/O 多路复用就像一个聪明的店员同时处理多张桌子上的顾客。通过使用一个线程进行多路复用,您可以高效地处理多个连接,避免了为每个连接都创建一个线程的开销。

想象一下您是一位大厨,正在一家繁忙的餐厅中忙碌。您需要同时管理多个订单,为每个订单做菜,并确保及时将菜品送到对应的桌子上。为了高效地处理这些订单,您可以使用epoll的思想,以及链表和红黑树的概念。

  1. 订单管理
    • epoll 类似于您的"订单通知系统"。您会将需要处理的订单(事件)添加到系统中,而不需要一直关注每个订单。
    • 链表就像您的"待处理订单列表"。您将新的订单放入列表,表示它们已经进入了系统,但您不会立即开始做菜。
    • 红黑树就像您的"正在做菜的订单列表"。当您开始为订单做菜时,将其放入红黑树中,以便在菜品完成后可以迅速找到对应的订单。
  2. 处理过程
    • 想象一下,您正在烹饪一道菜,突然听到订单通知系统响起。您查看链表中的待处理订单,发现有新的订单进来。
    • 您从待处理订单列表中选择一个订单,将其放入正在做菜的订单列表(红黑树)。然后,您开始为该订单做菜,这就好像在红黑树中插入一个节点。
    • 当您完成菜品时,您从红黑树中找到对应的订单,将菜品送到相应的桌子上。然后,您从红黑树中移除该订单,表示它已经完成。
  3. 效率提升
    • 使用epoll,您只需要等待订单通知系统响起,而不需要一直检查每个订单。
    • 链表帮助您记录新订单,而红黑树帮助您迅速找到正在处理的订单,以及完成的订单。

通过这个生动的例子,您可以将epoll、链表和红黑树的概念与餐厅的订单管理过程联系起来。这有助于理解它们在高效处理并发连接时的作用。记住,初学时可能会有些挑战,但随着实践和深入学习,您会逐渐掌握这些概念并提升信心。

  1. 中间件的执行顺序: 在ASP.NET Core中,每个HTTP请求都会经过一系列的中间件。中间件的执行顺序是按照它们被添加到应用程序中的顺序来决定的。请求首先进入第一个中间件,然后按照顺序依次通过其他中间件,最后进入处理终点(如控制器或静态文件处理器)。响应则会按照相反的顺序经过中间件,从处理终点返回到客户端。
  2. 中间件的结构: 中间件是一个C#类,它需要具备以下特点:
    • 具有一个接受 HttpContext 参数的构造函数。
    • 具有一个名为 InvokeInvokeAsync 的方法,用于处理请求和响应。
  3. 中间件的添加:Startup.cs文件中,可以使用app.UseMiddleware<T>方法将中间件添加到应用程序的请求处理管道中。你可以通过多次调用这个方法来添加多个中间件,它们将按照添加的顺序依次执行。
  4. 内置中间件: ASP.NET Core提供了一些内置的中间件,用于常见的任务,如路由、身份验证、静态文件服务等。你可以通过调用app.UseRoutingapp.UseAuthentication等方法来启用这些中间件。
  5. 自定义中间件: 你可以自己编写并添加自定义的中间件来满足应用程序特定的需求。自定义中间件可以用于执行各种任务,如性能监测、日志记录、请求修改等。

下面是一个简单的示例,展示如何创建一个简单的自定义中间件:

using System.Diagnostics;

namespace TEST0812
{
    public class TimingMiddleware
    {
        private readonly RequestDelegate _next;

        public TimingMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();

            await _next(context); // 调用下一个中间件或终点处理

            stopwatch.Stop();
            var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;

            // 记录请求处理时间
            Console.WriteLine($"Request for {context.Request.Path} took {elapsedMilliseconds} ms");
        }
    }
}

// Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // 添加自定义中间件到管道
    app.UseMiddleware<CustomMiddleware>();

    // ...
}
public class ErrorLoggingMiddleware
{
    private readonly RequestDelegate _next;

    public ErrorLoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context); // 调用下一个中间件或终点处理
        }
        catch (Exception ex)
        {
            LogError(ex);
            // 返回适当的错误响应,或者继续抛出异常以由其他中间件或终点处理进行处理
            throw;
        }
    }

    private void LogError(Exception ex)
    {
        // 将异常信息记录到日志文件
        string logMessage = $"{DateTime.UtcNow.ToString()} - Error: {ex.Message}\n";
        File.AppendAllText("error_log.txt", logMessage, Encoding.UTF8);
    }
}

权限管理

public async Task InvokeAsync(HttpContext context)
{
    string token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
    if (token != null)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_jwtService.SecretKey); // Replace with your key

        try
        {
            var claimsPrincipal = tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                // Validation parameters
            }, out SecurityToken validatedToken);

            // Extract user roles from claims or elsewhere
            var userRoles = GetUserRolesFromClaims(claimsPrincipal.Claims);

            // Create a new ClaimsIdentity with user roles
            var identity = new ClaimsIdentity(claimsPrincipal.Identity);
            foreach (var role in userRoles)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, role));
            }

            // Create a new ClaimsPrincipal with the updated identity
            var newClaimsPrincipal = new ClaimsPrincipal(identity);
            context.User = newClaimsPrincipal;
        }
        catch (Exception)
        {
            // Invalid token, continue with the next middleware or endpoint
        }
    }

    await _next(context);
}

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
    [HttpGet]
    [Authorize(Roles = "Admin, SuperAdmin")] // Only allow Admin and SuperAdmin roles
    public IActionResult GetAdminData()
    {
        // Handle admin data retrieval logic
        return Ok("Admin data");
    }
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Apply the authentication middleware to all routes except the login route
    app.Map("/login", loginApp =>
    {
        var username = context.Request.Form["username"];
        var password = context.Request.Form["password"];
        // Validate user credentials (this is just an example)
        if (IsValidUser(username, password))
        {
            // Generate JWT token
            var token = GenerateJwtToken(username); // Your token generation logic here
            context.Response.Headers.Add("Authorization", $"Bearer {token}");
            
            // Return success response
            context.Response.StatusCode = 200;
            await context.Response.WriteAsync("Login successful");
        }
        else
        {
            // Return failure response
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("Invalid credentials");
        }
    });

    // Use the authentication and authorization middleware for all other routes
    app.UseAuthentication();
    app.UseAuthorization();

    // Other middleware and routing configuration
    // ...
}

  • AddScoped:每个HTTP请求创建一个服务实例,请求内共享相同的实例。
  • AddTransient:每次请求创建一个新的服务实例。
  • AddSingleton:整个应用程序生命周期中只创建一个服务实例,所有请求共享相同的实例。
services.AddScoped<IMyService, MyService>(); // 使用接口和实现类注册
services.AddScoped<IMyService>(new MyService()); // 使用工厂方法注册,省略了 provider =>

services.AddScoped<IMyService, MyService>(); // 使用接口和实现类注册
services.AddScoped<IMyService>(new MyService()); // 使用工厂方法注册,省略了 provider =>

RESTful API是一种遵循REST原则的API设计风格。REST(Representational State Transfer)是一种基于HTTP协议的架构风格,它强调资源的抽象性和状态的转移。RESTful API允许客户端通过HTTP请求来执行各种操作,如获取资源、创建、更新和删除资源等。

RESTful API的关键特点包括:

  1. 无状态性(Statelessness): 每个请求都应该包含足够的信息,以便服务器可以理解请求,而不需要依赖之前的请求或状态。
  2. 资源(Resources): API的核心是资源,每个资源都有一个唯一的标识符(URL),客户端可以通过HTTP方法对资源进行操作。
  3. 统一接口(Uniform Interface): RESTful API应该使用统一的方法(如GET、POST、PUT、DELETE)和标准的HTTP状态码来进行操作和传达状态。
  4. 自描述性(Self-descriptive): API的请求和响应应该是自描述的,包含足够的信息,使开发人员能够理解如何使用API。
  5. 客户端-服务器分离(Client-Server Separation): 客户端和服务器应该独立地演化,使得客户端和服务器可以分别进行优化。
  6. 资源命名: 使用清晰的、有意义的资源命名,将资源表示为URL路径的一部分。
  7. 使用HTTP方法: 使用合适的HTTP方法来执行不同的操作,如GET(获取)、POST(创建)、PUT(更新)、DELETE(删除)等。