IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 【网络编程知识】使用Socket通信,做一个简单的多人聊天室 -> 正文阅读

[网络协议]【网络编程知识】使用Socket通信,做一个简单的多人聊天室

在这里插入图片描述


📢前言

  • Socket 作为网络编程超级重要的一个知识点,对于程序员自然是一个必备的东西。
  • 正好最近在看一些Socket套接字的内容,就想着能不能使用Socket做一个简单的通信功能,来加深对Socket的认知。
  • 说到Socket那自然少不了TCP/IP协议,想了解这块可以参考另一篇文章:Socket基本概念简单理解
  • 所以这篇文章就来使用Socket来简单制作一个多人聊天室,看看Socket到底怎样进行使用。
  • 本文使用 Unity引擎 + C# 实现,制作过程还是挺简单的,下面一起来看看吧!

🎬使用Socket通信,做一个简单的多人聊天室

请添加图片描述

套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
在这里插入图片描述
要使用Socket制作一个简单的多人聊天室,需要有 服务端 和 客户端。

一个简单的 基于 TCP 协议的 客户端服务端 工作流程如下:
在这里插入图片描述

  • 服务端 和 客户端初始化 socket,得到文件描述符
  • 服务端 调用 bind,绑定 IP 地址和端口号
  • 服务端 调用 listen,进行监听 并 设置最大连接数量
  • 服务端 调用 accept,等待接收 客户端 连接
  • 客户端 调用 connect,向服务器端的地址和端口发起连接请求
  • 服务端 accept 返回用于传输的 socket 的文件描述符
  • 客户端 调用 send 写入数据
  • 服务端调用 recv 读取数据
  • 客户端 断开连接时,会调用 close,那么 服务端 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端 调用 close,表示连接关闭

先来说一下这个 多人聊天室 要满足的基本功能

  1. 有一个服务端 和 多个 客户端,服务端 建立监听,客户端 进行连接。
  2. 当有 新的客户端 连接成功时,会给 其他客户端 发送一条消息。
  3. 客户端 给 服务端 发消息时,服务端 把收到的消息转发给 除客户端本身(发消息的客户端) 外的 所有客户端。

这样就实现了一个 简单的多人即时通信聊天室 的功能。
下面就开始编写 服务端 和 客户端 的代码,使其建立连接实现通信。


🏳??🌈一个简单的UI

按照自己的需求喜好制作一个简易的聊天室。

必要条件:

  • 一个服务端开启的Button按钮
  • 客户端连接到服务器的按钮(自己几个随意)
  • 消息输入框InputField 和 内容显示框Text(跟随客户端数量添加)
    在这里插入图片描述

🏳??🌈服务端 部分

服务端 部分大致步骤操作如下:

  • 第一步:创建一个服务器Socket对象。
  • 第二步:等待客户端的连接 并且创建与之通信的Socket
  • 第三步:服务器端不停的接收客户端发来的消息
  • 第四步:服务器向客户端发送消息

先在服务器创建一个Socket对象,绑定IP和端口号,设置最大连接数量,创建一个线程进行监听。
然后等待客户端的连接,并将已连接的客户端存取起来,有客户端加入连接时广播消息。
服务器端不停的接收客户端发来的消息,将消息字节流转化为字符串并群发消息。

新建脚本SocketServer.cs,完整代码如下:

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;

public class SocketServer : MonoBehaviour
{
    private List<string> _ClientList;
    private List<Socket> _SocketsList;
    private Dictionary<string, string> _ClientName;

    private void Awake()
    {
        _ClientList = new List<string>();
        _SocketsList = new List<Socket>();
        _ClientName = new Dictionary<string, string>();
    }

    public void StartServer()
    {
        //服务器连接 按钮点击事件
        bt_connnect_Click();
    }

    /// <summary>
    /// 第一步:创建一个服务器Socket对象
    /// </summary>
    private void bt_connnect_Click()
    {
        try
        {
            int _port = 6000;
            string _ip = "127.0.0.1";

            //点击开始监听时 在服务端创建一个负责监听IP和端口号的Socket
            Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress ip = IPAddress.Parse(_ip);
            //创建对象端口
            IPEndPoint point = new IPEndPoint(ip, _port);

            //绑定端口号
            socketWatch.Bind(point);
            Debug.Log("监听成功!");
            //设置监听,最大同时连接10台
            socketWatch.Listen(10);

            //创建监听线程
            Thread thread = new Thread(Listen);
            thread.IsBackground = true;
            thread.Start(socketWatch);
        }
        catch { }
    }

    /// <summary>
    /// 第二步:等待客户端的连接 并且创建与之通信的Socket
    /// </summary>
    Socket socketSend;
    void Listen(object o)
    {
        try
        {
            int client=1;
            Socket socketWatch = o as Socket;
            while (true)
            {
                socketSend = socketWatch.Accept();//等待接收客户端连接
                Debug.Log(socketSend.RemoteEndPoint.ToString() + ":" + "连接成功!");

                //将连接到服务器的客户端添加到列表中
                _ClientList.Add(socketSend.RemoteEndPoint.ToString());
                _SocketsList.Add(socketSend);
                _ClientName.Add(socketSend.RemoteEndPoint.ToString(), client+"号");
                client++;

                for (int i = 0; i <= _ClientList.Count - 1; i++)
                {
                    //服务器向所有客户端转发消息,除了发这条消息的客户端本身
                    if (socketSend.RemoteEndPoint.ToString() != _ClientList[i])
                    {
                        Sends(_ClientName[socketSend.RemoteEndPoint.ToString()]+ "加入房间\n" ,  i);
                    }
                    
                }

                //开启一个新线程,执行接收消息方法
                Thread r_thread = new Thread(Received);
                r_thread.IsBackground = true;
                r_thread.Start(socketSend);
            }
        }
        catch(Exception e)
        {
            Debug.Log(e);
        }
    }

    /// <summary>
    /// 第三步:服务器端不停的接收客户端发来的消息
    /// </summary>
    /// <param name="o"></param>
    void Received(object o)
    {
        try
        {
            Socket socketSend = o as Socket;
            while (true)
            {
                //客户端连接服务器成功后,服务器接收客户端发送的消息
                byte[] buffer = new byte[1024 * 1024 * 3];
                //实际接收到的有效字节数
                int len = socketSend.Receive(buffer);
                if (len == 0)
                {
                    break;
                }
                string str = Encoding.UTF8.GetString(buffer, 0, len);
                Debug.Log("服务器打印:" + socketSend.RemoteEndPoint + ":" + str);

                for (int i=0;i<= _ClientList.Count-1;i++)
                {
                    //服务器向所有客户端转发消息,除了发这条消息的客户端本身
                    if (socketSend.RemoteEndPoint.ToString() != _ClientList[i])
                    {
                        Sends(_ClientName[socketSend.RemoteEndPoint.ToString()] + ":" + str,i);
                    }
                }
            }
        }
        catch (Exception e)
        {
            Debug.Log(e);
        }
    }

    /// <summary>
    /// 第四步:服务器向客户端发送消息
    /// </summary>
    /// <param name="str"></param>
    void Sends(string msg,int socket)
    {
        byte[] buffer = Encoding.UTF8.GetBytes(msg);
        _SocketsList[socket].Send(buffer);
    }
}

🏳??🌈客户端部分

客户端部分 比 服务端 简单,大致步骤如下:

  • 第一步:客户端连接到服务器
  • 第二步:接收服务端返回的消息
  • 第三步:向服务器发送消息

这部分和服务端其实差不多,就是从监听改为了连接到服务端。
然后使用了Loom插件从多线程中给主线程中的UI添加内容,对Loom插件使用不熟悉的可以参考这篇文章:

Unity零基础到进阶 | Unity中的多线程的使用,普通创建Thread + 使用Loom插件创建

当然也可以不使用Loom插件来进行Socket通信,我这里只是简单利用Loom插件来将服务端发来的消息直接绘制在聊天框里了。

也可以自己将服务端发来的内容通过别的方式发送到主线程的UI中,避免增加项目的复杂度。

新建脚本SocketClient.cs,完整代码如下:

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;

public class SocketClient : MonoBehaviour
{
    public InputField input;//聊天输入框
    public Text text;//聊天内容显示框

    public void StartClient()
    {
        bt_connect_Click();
    }

    public void SendMsg()
    {
        bt_send_Click(input.text);
    }

    Socket socketSend;
    /// <summary>
    /// 第一步:客户端连接到服务器
    /// </summary>
    private void bt_connect_Click()
    {
        try
        {
            int _port = 6000;
            string _ip = "127.0.0.1";

            //创建客户端Socket,获得远程ip和端口号
            socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress ip = IPAddress.Parse(_ip);
            IPEndPoint point = new IPEndPoint(ip, _port);

            //向服务器端的地址和端口发起连接请求
            socketSend.Connect(point);

            Loom.RunAsync(() =>
            {
                //开启新的线程,不停的接收服务器发来的消息
                Thread c_thread = new Thread(Received);
                c_thread.IsBackground = true;
                c_thread.Start();
            });
        }
        catch (Exception)
        {
            Debug.Log("IP或者端口号错误...");
        }
    }

    /// <summary>
    /// 第二步:接收服务端返回的消息
    /// </summary>
    void Received()
    {
        while (true)
        {
            try
            {
                byte[] buffer = new byte[1024 * 1024 * 3];
                //实际接收到的有效字节数
                int len = socketSend.Receive(buffer);
                if (len == 0)
                {
                    break;
                }
                string str = Encoding.UTF8.GetString(buffer, 0, len);
                Debug.Log("客户端打印:" + socketSend.RemoteEndPoint + ":" + str);

                //使用Loom调用主线程的内容
                Loom.QueueOnMainThread((param) =>
                {
                    text.text += str + "\n";
                }, null);
            }
            catch (Exception e)
            {
                Debug.Log("错误..."+e);
            }
        }
    }

    /// <summary>
    /// 第三步:向服务器发送消息
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void bt_send_Click(string str)
    {
        try
        {
            string msg = str;
            byte[] buffer = new byte[1024 * 1024 * 3];
            buffer = Encoding.UTF8.GetBytes(msg);
            socketSend.Send(buffer);

            text.text += "我:" + str + "\n";
            input.text = "";
        }
        catch { }
    }
}   

🏳??🌈Loom插件的使用

Loom插件 就一个脚本,目的是可以从多线程中调用Unity的UI部分。
在上面的客户端中用到,直接将Loom脚本挂在到场景中就可以使用。

Loom脚本完整代码如下:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Threading;
using System.Linq;

public class Loom : MonoBehaviour
{
    public static int maxThreads = 8;
    static int numThreads;

    private static Loom _current;
    //private int _count;
    public static Loom Current
    {
        get
        {
            Initialize();
            return _current;
        }
    }

    void Awake()
    {
        _current = this;
        initialized = true;
    }

    static bool initialized;

    public static void Initialize()
    {
        if (!initialized)
        {

            if (!Application.isPlaying)
                return;
            initialized = true;
            var g = new GameObject("Loom");
            _current = g.AddComponent<Loom>();
#if !ARTIST_BUILD
            UnityEngine.Object.DontDestroyOnLoad(g);
#endif
        }

    }
    public struct NoDelayedQueueItem
    {
        public Action<object> action;
        public object param;
    }

    private List<NoDelayedQueueItem> _actions = new List<NoDelayedQueueItem>();
    public struct DelayedQueueItem
    {
        public float time;
        public Action<object> action;
        public object param;
    }
    private List<DelayedQueueItem> _delayed = new List<DelayedQueueItem>();

    List<DelayedQueueItem> _currentDelayed = new List<DelayedQueueItem>();

    public static void QueueOnMainThread(Action<object> taction, object tparam)
    {
        QueueOnMainThread(taction, tparam, 0f);
    }
    public static void QueueOnMainThread(Action<object> taction, object tparam, float time)
    {
        if (time != 0)
        {
            lock (Current._delayed)
            {
                Current._delayed.Add(new DelayedQueueItem { time = Time.time + time, action = taction, param = tparam });
            }
        }
        else
        {
            lock (Current._actions)
            {
                Current._actions.Add(new NoDelayedQueueItem { action = taction, param = tparam });
            }
        }
    }

    public static Thread RunAsync(Action a)
    {
        Initialize();
        while (numThreads >= maxThreads)
        {
            Thread.Sleep(100);
        }
        Interlocked.Increment(ref numThreads);
        ThreadPool.QueueUserWorkItem(RunAction, a);
        return null;
    }

    private static void RunAction(object action)
    {
        try
        {
            ((Action)action)();
        }
        catch
        {
        }
        finally
        {
            Interlocked.Decrement(ref numThreads);
        }
    }

    void OnDisable()
    {
        if (_current == this)
        {

            _current = null;
        }
    }



    // Use this for initialization
    void Start()
    {

    }

    List<NoDelayedQueueItem> _currentActions = new List<NoDelayedQueueItem>();

    // Update is called once per frame
    void Update()
    {
        if (_actions.Count > 0)
        {
            lock (_actions)
            {
                _currentActions.Clear();
                _currentActions.AddRange(_actions);
                _actions.Clear();
            }
            for (int i = 0; i < _currentActions.Count; i++)
            {
                _currentActions[i].action(_currentActions[i].param);
            }
        }

        if (_delayed.Count > 0)
        {
            lock (_delayed)
            {
                _currentDelayed.Clear();
                _currentDelayed.AddRange(_delayed.Where(d => d.time <= Time.time));
                for (int i = 0; i < _currentDelayed.Count; i++)
                {
                    _delayed.Remove(_currentDelayed[i]);
                }
            }

            for (int i = 0; i < _currentDelayed.Count; i++)
            {
                _currentDelayed[i].action(_currentDelayed[i].param);
            }
        }
    }
}

🎁效果展示

写完脚本之后将其挂在场景中,把Button按钮的监听事件设置成对应的方法就可以了。

我这是的设置是服务端自动将加入的客户端命名为1号、2号。
还可以在客户端进行连接成功的时候,再给服务端发送一个自定义的昵称,这样效果会更好一下。
大家也可以自己去改改代码尝试一下。

下面是直接在Unity编辑器运行的效果,有两个客户端。
请添加图片描述

再来看一下四个客户端的效果,编辑器加上打包出来的应用一起连接:
请添加图片描述


💬总结

  • 本文使用 Socket通信 做了一个多人聊天的简单实例。
  • 客户端一个脚本,服务端一个脚本,再加一个工具脚本Loom就完成了这个简单的Socket多人聊天实例。
  • 主要是了解Socket通信的基本流程 和 几个步骤,让我们更好的认识Socket。
  • 整体执行代码还是很好理解的,但是真正的项目中Socket使用要比这复杂的多。
  • 所以想学会一个知识点,还是要多学多练习呀~

温馨提示: 点击下面卡片可以获取更多编程知识,包括各种语言学习资料,上千套PPT模板和各种游戏源码素材等等资料。更多内容可自行查看哦!
请添加图片描述

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-03-21 21:30:49  更:2022-03-21 21:31:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 5:24:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码