温故之.NET Socket通信

2018-07-21  本文已影响0人  JameLee

上一篇文章介绍了内存映射文件,这篇文章我们介绍一种用得更加广泛的方式——Socket 通信

Socket 介绍

Socket 称为”套接字”,它分为流式套接字和用户数据报套接字,分别对应网络中的 TCP 和 UDP 协议。这两种均可以实现进程间通信(无论是否是同一机器)

TCP 协议是面向连接的协议,提供稳定的双向通信功能,TCP连接的建立是通过三次握手才能完成,稳定性高,创建连接的效率相对UDP较低

UDP协议是面向无连接的,效率高,但不保证数据一定能够正确传输(顺序、丢包等)

我们应该选择 UDP 还是 TCP?

但实际项目中,这样“纯粹”的场景并不是那么多,因此,往往采用的方案都是 TCP、UDP 相结合的方式来实现。当然为了保证数据的可靠及业务的稳定性,很多框架都不仅仅只有这么两种技术

框架的复杂、轻量与否,与其应对的业务场景是相关的。我们需要根据不同的场景,来选择适合自己项目的框架。在 C# 中,有 FastSocketSuperSocketSocket 框架供大家选择。其中 SuperSocket 支持 IOCP,它可以实现高性能、高并发。其他语言有 NettyHP-Socket 等,这些也有 .NET 的移植版本

一般情况下,不建议各位朋友自己去写一个 Socket 框架来支持项目的业务场景,用现有的框架更加稳当。如果不知道选择什么框架,可以去 Github 上搜索相关的开源框架

选择 Github 中的框架时,我们应该注意

Socket 通信,是市面上很多框架的基础,因此我们有必要介绍下它的使用方式,及在开发过程中需要注意的事项

使用示例

在 C# 中,无论是 TCP 协议,还是 UDP 协议,都封装在了 Socket 这个类中。使用时,只需要我们指定不同的参数即可

TCP 与 UDP 区别

在大部分情况下(针对性能而言),我们无法感觉到这两者之间的差异;而在高并发的场景下,我们就能很容易体会到(因为访问量大了之后,任何细小的变化都能累积起来从而造成巨大的影响)

使用 TCP 面临的一个主要问题就是粘包,业界主流的解决方案可归纳如下

另外,如果觉得自定义协议太麻烦,我们也可以根据 MQTT 协议来写一套符合它的解决方案

针对 TCP 的使用,我们给出一个例子。其中我们采用 Jil 来实现序列化

/// <summary>
/// 传输使用的包
/// </summary>
public class Packet {
    public const int TYPE_LOGIN = 10001;
    public const int TYPE_MSG = 10000;
    public const int TYPE_LOGOUT = 10002;
    public const int TYPE_INVALID = 40000;

    /// <summary>
    /// 这个包的类型。在实际业务场景中,一般会使用 int、short 等来表示,而不是 enum
    /// </summary>
    public int Type { get; set; }
    /// <summary>
    /// 具体的业务数据
    /// </summary>
    public string Data { get; set; }
}

以下为服务端代码

using Jil;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace App {
    class Program {
        static void Main(string[] args) {
            TcpListener tcpListener = new TcpListener(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9999));
            tcpListener.Start();
            /// 此处仅仅用于处理客户端的连接
            /// 而不涉及具体的业务逻辑
            while (true) {
                TcpClient remoteClient = tcpListener.AcceptTcpClient();
                ClientPacketHandlers packetHandlers = new ClientPacketHandlers(remoteClient);
            }
        }

    }

    /// <summary>
    /// 将业务逻辑处理分开
    /// </summary>
    public class ClientPacketHandlers {
        Dictionary<int, Action<NetworkStream, string>> clientHandlers = new Dictionary<int, Action<NetworkStream, string>>();
        TcpClient remoteClient;
        NetworkStream stream;
        Task processTask;
        CancellationTokenSource cancellationTokenSource;

        public ClientPacketHandlers(TcpClient client) {
            this.remoteClient = client;
            this.stream = remoteClient.GetStream();

            // 这个可以通过配置文件来添加处理器
            clientHandlers.Add(Packet.TYPE_LOGIN, HandleLogin);
            clientHandlers.Add(Packet.TYPE_MSG, HandleMsg);
            clientHandlers.Add(Packet.TYPE_LOGOUT, HandleLogout);

            cancellationTokenSource = new CancellationTokenSource();

            // 为该客户端开辟一个 Task,用于与该客户端通信
            // 在高并发场景中,往往不会这样做。而是采用 IOCP 或者其他的高性能的方式
            // 为每个客户端开辟一个 Task 不合理,也很浪费系统资源(因为不是每个客户端都会频繁发送消息)
            processTask = Task.Run(() => {
                byte[] buffer = new byte[1024];
                while (true) {
                    int bytesRead = stream.Read(buffer, 0, 1024);
                    if (bytesRead > 0) {
                        byte[] realBytes = new byte[bytesRead];
                        Buffer.BlockCopy(buffer, 0, realBytes, 0, bytesRead);

                        Packet packet = JSON.Deserialize<Packet>(Encoding.UTF8.GetString(realBytes));
                        if (packet != null) {
                            if (clientHandlers.ContainsKey(packet.Type)) {
                                clientHandlers[packet.Type].Invoke(stream, packet.Data);
                            } else {
                                SendPacket(stream, new Packet() { Type = Packet.TYPE_INVALID, Data = "No handlers for your type" });
                            }
                        }
                    }

                    if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) {
                        break;
                    }
                }
            }, cancellationTokenSource.Token);
        }

        public void HandleLogin(NetworkStream stream, string data) {
            if (stream == null || string.IsNullOrEmpty(data)) return;
            SendPacket(stream, new Packet() { Type = Packet.TYPE_LOGIN, Data = $"Hello, {data}" });
        }

        public void HandleMsg(NetworkStream stream, string data) {
            if (stream == null || string.IsNullOrEmpty(data)) return;
            SendPacket(stream, new Packet() { Type = Packet.TYPE_MSG, Data = $"Received Msg : {data}" });
        }

        public void HandleLogout(NetworkStream stream, string data) {
            if (stream == null || string.IsNullOrEmpty(data)) return;
            SendPacket(stream, new Packet() { Type = Packet.TYPE_LOGOUT, Data = $"Logout, {data}" });
            try {
                if (cancellationTokenSource != null) {
                    cancellationTokenSource.Cancel();
                    cancellationTokenSource.Dispose();
                }
            } catch (Exception e) {
            } finally {
                cancellationTokenSource = null;
            }
        }


        public void SendPacket(NetworkStream stream, Packet packet) {
            byte[] packetBytes = Encoding.UTF8.GetBytes(JSON.Serialize(packet));
            stream.Write(packetBytes, 0, packetBytes.Length);
        }
    }
}

以下为客户端代码

using Jil;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace App {
    class Program {
        static void Main(string[] args) {
            TcpClient tcpClient = new TcpClient();
            tcpClient.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9999));
            NetworkStream networkStream = tcpClient.GetStream();

            Task.Run(() => {
                byte[] buffer = new byte[1024];
                while (true) {
                    int bytesRead = networkStream.Read(buffer, 0, 1024);
                    if (bytesRead > 0) {
                        byte[] realBytes = new byte[bytesRead];
                        Buffer.BlockCopy(buffer, 0, realBytes, 0, bytesRead);

                        Packet packet = JSON.Deserialize<Packet>(Encoding.UTF8.GetString(realBytes));
                        if (packet != null) {
                            Console.WriteLine($"RECEIVED DATA: {packet.Data}");
                        }
                    }
                }
            });

            while (true) {
                string line = Console.ReadLine();
                string[] strs = line.Split(':');
                if(strs.Length >= 2) {
                    if(strs[0] == "login") {
                        SendPacket(networkStream, new Packet() { Type = Packet.TYPE_LOGIN, Data = strs[1] });
                    } else if (strs[0] == "msg") {
                        SendPacket(networkStream, new Packet() { Type = Packet.TYPE_MSG, Data = strs[1] });
                    } else if (strs[0] == "logout") {
                        SendPacket(networkStream, new Packet() { Type = Packet.TYPE_LOGOUT, Data = strs[1] });
                    }
                }
            }
        }

        private static void SendPacket(NetworkStream networkStream, Packet packet) {
            byte[] packetBytes = Encoding.UTF8.GetBytes(JSON.Serialize(packet));
            networkStream.Write(packetBytes, 0, packetBytes.Length);
        }
    }
}

这便是 TCP 通信的基础示例了,在更复杂的场景中,系统的设计将会更加复杂。但宗旨都只有一个,提供更加稳定可靠的服务

UDP 的使用与 TCP 类似,因此就不一一举例了

开发建议



至此,这篇文章的内容讲解完毕。欢迎关注公众号【嘿嘿的学习日记】,所有的文章,都会在公众号首发,Thank you~

公众号二维码
上一篇下一篇

猜你喜欢

热点阅读