Socket网络通信(3

  在上次的笔记中,其实是留了一个小的隐患,做过测试的人可能会发现,客户端接收数据只能接收一个,因为Accept方法是个阻塞方法,为了让客户端或服务器一直去接收消息,我们可以用个While循环,让socket一直处于接收状态。

                while (true)

                    {

                        //等待客户端连入

                        Socket client = socket.Accept();

                        Console.WriteLine("客户端连入");

                  }

  上次说了同步Socket程序,今天呢,说一下异步Socket程序。

同步程序中,服务器使用Accept接收请求,客户端使用Connect连接服务器。相对的,在异步模式下,服务器可以使用BeginAcceptEndAccept方法完成连接到客户端的任务。我会通过一个多人聊天的例子来说明异步Socket程序。

     Socket里面的Begin开头的Api基本都是异步方法。涉及到的知识点大家自己去了解吧,比如BeginAccept,BeginReceive等。

我说一下多人聊天的思路:

  首先我会用C#写一个后台的服务器,服务器的逻辑处理我会通过控制台展示出来。

  然后用Unity去搭建UI,把客户端的逻辑放在它里面,让它去更新消息。

  后台和前台共同持有的类我写在一个类库文件里面了,这样我可以把我的类库文件生成.dll文件,把它分别添加到前后端项目中。我说一下用类库文件的优点,如果随着项目的扩展,前后端共同持有的类也会越来越多,那么我们可以通过这种方式,很方便的实现前后端的数据类同步。如果在同一台电脑上的两个项目也都持有我这个类库文件。那么我如果更新了类库文件,重新生成之后。这两个项目中的类库文件也都会随着更新。这个多人聊天例子中呢,并没有非常多的数据,也只是需要发送和接受一些字符串数据,所以这里并没有写那么多的数据类,当然如果在我们的项目中,数据通信模块,与其他模块的交互肯定会有更多的数据类,比如与装备模块,与任务模块,与人物信息模块等的交互,会发送不同的数据,前后端肯定会有很多共同的数据类要持有。

     闲话不多说了,直接上代码吧! 我们知道服务器需要处理多个客户端连接,它需要用一个数组来维护所有客户端的连接。每个客户端都有自己的Socket和缓冲区。所以我在服务端项目中定义了Conn类作为缓冲区,同时它也是服务端程序的重要数据结构。

using System;

using System.Collections;

using System.Collections.Generic;

using System.Net.Sockets;

 

namespace Common

{

/// <summary>

/// 表示客户端连接的类

/// </summary>

public class Conn

{

    /// <summary>

    /// 常量

    /// </summary>

    public const int BUFFER_SIZE = 1024;

 

    /// <summary>

    /// Socket

    /// </summary>   

    public Socket socket;

 

    /// <summary>

    /// 是否在使用

    /// </summary>

    public bool isUse =false;

 

    /// <summary>

    /// 缓冲区Buff

    /// </summary>

    public byte[] readBuffer =null;

 

    public int bufferCount = 0;

 

    /// <summary>

    ///构造函数

    /// </summary>

    /// <param name="socket"></param>

    public Conn()

    {

        readBuffer = new byte[BUFFER_SIZE];

    }

 

    /// <summary>

    ///  初始化, 在实例化的时候把对应的Socket传进来,表示该socket可用,bufferCount默认为0

    /// </summary>

    /// <param name="socket"></param>

    public void Init(Socket socket)

    {

        this.socket = socket;

        isUse = true;

        bufferCount = 0;

    }

 

    /// <summary>

    /// 缓冲区剩余的字节数

    /// </summary>

    /// <returns></returns>

    public int BufferRemain()

    {

        return BUFFER_SIZE - bufferCount;

    }

 

    /// <summary>

    /// 获取当前客户端socket的IP地址和端口

    /// </summary>

    /// <returns></returns>

    public string GetAdress()

    {

        if (!isUse)

            return "无法获取地址";

        return socket.RemoteEndPoint.ToString();

    }

 

    /// <summary>

    /// 关闭当前客户端连接

    /// </summary>

    public void Close()

    {

        if (!isUse)

            return;

        Console.WriteLine("[断开连接]" + GetAdress());

        socket.Close();

        isUse = false;

    }

}

}

接下来就是服务端代码。都写了详细的注释。

using Common;

using System;

using System.Net;

using System.Net.Sockets;

 

namespace Server

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("hello,Game");

 

            //开启服务器

            Server server = new Server();

            server.Start("127.0.0.1", 1234);

 

            //如果输入quit,退出程序

            while (true)

            {

                string str = Console.ReadLine();

                switch (str)

                {

                    case "quit":

                        return;

                }

            }

        }

    }

/// <summary>

///服务端程序类

/// 它包含一个Conn类型的对象池(数组),用于维护客户端的连接。

/// </summary>

public class Server

    {

 

        /// <summary>

        /// 服务端监听套接字

        /// </summary>

        public Socket listenSocket;

 

        /// <summary>

        /// 客户端连接池

        /// </summary>

        public Conn[] conns;

 

        /// <summary>

        ///最大连接数,监听队列最大个数

        /// </summary>

        public int maxConn = 50;

 

        /// <summary>

        ///获取连接池的索引,如果返回负数,表示获取失败

        /// 方便异步接受客户端的Socket的调用

        /// </summary>

        /// <returns>The index.</returns>

        public int NewIndex()

        {

            //如果当前数组为空,则获取失败,否则遍历该数组找出空的位置,创建一个Conn连接类,并返回索引,或者返回一个isUse为false的conn的索引。

            if (conns == null)

                return -1;

            for (int i = 0; i < conns.Length; i++)

            {

                if (conns[i] == null)

                {

                    conns[i] = new Conn();

                    return i;

                }

                else if (conns[i].isUse ==false)

                {

                    return i;

                }

            }

            return -1;

        }

 

 

        /// <summary>

        /// 开启服务器

        /// </summary>

        /// <param name="host">Host.</param>

        /// <param name="port">Port.</param>

        public void Start(string host,int port)

        {

            //连接池初始化,并将每一个Conn实例化,涉及知识:连接池的核心思想是连接复用,通过建立一个连接池及一套连接使用、分配、管理的策略,

            //使得不必每次新建连接都要生成Conn实例。因为服务端对性能要求比较高,生成Conn实例将涉及内存的申请分配等操作,相对比价好费时间

            //(尽管是很短很短的时间)。连接池因为使用了固定数组,会占用更多的内存,所以它是一个“空间换时间”的策略。

            conns = new Conn[maxConn];

            for (int i = 0; i < conns.Length; i++)

            {

                conns[i] = new Conn();

            }

 

            //Socket服务端监听套接字

            listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

 

            //Bind

            IPAddress ipAdr = IPAddress.Parse(host);

            IPEndPoint ipEp = new IPEndPoint(ipAdr, port);

            listenSocket.Bind(ipEp);

 

            //Listen

            listenSocket.Listen(maxConn);

 

            //Accept

            listenSocket.BeginAccept(AcceptCb, null);

            Console.WriteLine("[服务器]启动成功");

        }

 

        /// <summary>

        /// Accept回调,BeginAccept的回调函数。它里面处理了三件事情。

        /// 1.给连接分配Conn

        /// 2.异步接受客户端数据。

        /// 3.再次调用AcceptCb实现循环接收。

        /// </summary>

        /// <param name="ar">Ar.</param>

        public void AcceptCb(IAsyncResult ar)

        {

            //这里用了一个Try-Catch结构,它呢是专门处理异常的结构。它允许将任何可能发生异常情形的程序代码放置在try{}中进行监控

            //如Accept方法,因为它是个阻塞方法,可能当我这边的客户端断开或关闭的时候,服务端Accept接收失败,它就会报出异常,

            //若有异常发生,catch{}里面的代码将会被执行。catch语句中的Exception e附带了异常信息,可以将其打印出来。

            try

            {

                Socket socket = listenSocket.EndAccept(ar);

                int index = NewIndex();

 

                //如果小于0,表示连接池已满,则拒绝连接,否则给新的连接分配Conn

                if (index < 0)

                {

                    socket.Close();

                    

                   

                    Console.WriteLine("[警告]连接已满");

                }

                else

                {

                    //给新的连接分配Conn

                    Conn conn = conns[index];

                    conn.Init(socket);

                    string adr = conn.GetAdress();

                    Console.WriteLine("客户端连接[" + adr +"] conn池ID:" + index);

 

                    //异步接收客户端的数据

                    socket.BeginReceive(conn.readBuffer, conn.bufferCount, conn.BufferRemain(), SocketFlags.None, ReceiveCb, conn);

 

                    //再次调用BeginAccept实现循环

                    listenSocket.BeginAccept(AcceptCb, null);

                }

            }

            catch (Exception e)

            {

 

                Console.WriteLine("AcceptCb失败:" + e.Message);

            }

        }

 

        /// <summary>

        /// Receive回调,BeginReceive的回调函数,它里面处理了三件事情。

        /// 1.接收并处理消息,因为是多人聊天,服务端收到消息后,要把它转发给所有人。

        /// 2.如果收到客户端关闭连接的信号(count==0),则断开连接。

        /// 3.继续调用ReceiveCb接收下一个数据

        /// </summary>

        /// <param name="ar">Ar.</param>

        public void ReceiveCb(IAsyncResult ar)

        {

            //获取BeginReeive传入的Conn对象

            Conn conn = (Conn)ar.AsyncState;

            try

            {

                //获取接收的字节数

                int count = conn.socket.EndReceive(ar);

 

                //关闭信号

                if (count <= 0)

                {

                    Console.WriteLine("收到[" + conn.GetAdress() +"]断开连接");

                    conn.Close();

                    return;

                }

                //数据处理

                string str = System.Text.Encoding.UTF8.GetString(conn.readBuffer, 0, count);

                Console.WriteLine("收到[" + conn.GetAdress() +"]数据:" + str);

                str = conn.GetAdress() + ":" + str;

                byte[] bytes = System.Text.Encoding.Default.GetBytes(str);

 

                //广播,将消息发送给所有正在使用的连接

                for (int i = 0; i < conns.Length; i++)

                {

                    if (conns[i] == null)

                        continue;

                    if (!conns[i].isUse)

                        continue;

                    Console.WriteLine("将消息转播给" + conns[i].GetAdress());

                    conns[i].socket.Send(bytes);

                }

                //继续接收

                conn.socket.BeginReceive(conn.readBuffer, conn.bufferCount, conn.BufferRemain(), SocketFlags.None, ReceiveCb, conn);

 

            }

            catch (Exception ex)

            {

                Console.WriteLine("收到[" + conn.GetAdress() +"]断开连接");

                conn.Close();

            }

        }

    }

 

}

后台写完了,该前台了。下面是用NGUI搭建的UI,有点丑勿喷。

 Socket网络通信(三)

相应的客户端代码如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using System.Net;

using System.Net.Sockets;

using System;

 

/// <summary>

///客户端程序

/// </summary>

public class Net : MonoBehaviour {

 

/// <summary>

/// 服务器Ip和端口号Input

/// </summary>

public UIInput hostInput;

public UIInput portInput;

 

/// <summary>

/// 聊天区域

/// </summary>

public UITextList uiTextList;

 

//收到的消息

public string recvStr;

 

/// <summary>

/// 聊天输入框

/// </summary>

public UIInput chatInput;

 

/// <summary>

///客户端Ip和端口Lable

/// </summary>

public UILabel clientIpLable;

 

    /// <summary>

    /// buttons

    /// </summary>

    public UIButton connectBtn;

    public UIButton sendBtn;

 

/// <summary>

///Socket

/// </summary>

private Socket socket;

 

/// <summary>

///常量buffer的最大个数

/// </summary>

public const int BUFFER_SIZE=1024;

 

/// <summary>

///接收缓冲区

/// </summary>

public byte[] readBuffer =null;

 

public Stack<string> messages=null;

 

    private void Awake()

    {

        readBuffer = new byte[BUFFER_SIZE];

        messages = new Stack<string>();

    }

 

    /// <summary>

    ///连接服务器

    /// "连接服务器按钮"按下时,调用此方法。它依照socket-connect的流程连接到服务端,然后调用BeginReceive开启异步接收

    /// </summary>

    public void Connection()

{

        recvStr = "";

//Socekt

socket=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);

 

//connect

string host=hostInput.value;

int port = int.Parse(portInput.value);

socket.Connect (host, port);

clientIpLable.text = socket.LocalEndPoint.ToString ();

 

//Receive

socket.BeginReceive(readBuffer,0,BUFFER_SIZE,SocketFlags.None,ReceiveCb,null);

}

 

/// <summary>

/// 接收回调

///当收到服务端发来的消息时,异步接收回调ReceiveCb将被调用。它先是解析服务端的消息,将聊天语句存入recvStr中,然后再调用BeginReceive开启下一次异步接收。

/// </summary>

/// <param name="ar">Ar.</param>

public void ReceiveCb(IAsyncResult ar)

{

try {

//count是接受数据的大小

int count= socket.EndReceive (ar);

 

//数据处理

string str=System.Text.Encoding.UTF8.GetString(readBuffer,0,count);

if (recvStr.Length > 300)

recvStr = "";

recvStr+=str;

            //发消息提醒前台更新UI

            SendMessage(recvStr);

//继续接收

socket.BeginReceive(readBuffer,0,BUFFER_SIZE,SocketFlags.None,ReceiveCb,null);

 

} catch (Exception ex) {

recvStr = "连接已断开";

            SendMessage(recvStr);

socket.Close ();

}

}

 

/// <summary>

/// 发送数据

/// 当点击“发送”按钮时,执行该方法。它将输入框的内容发送给服务器

/// </summary>

public void Send()

{

string str = chatInput.value;

byte[] bytes = System.Text.Encoding.UTF8.GetBytes (str);

try {

socket.Send(bytes);

            chatInput.text = "";

} catch (Exception ex) {

}

}

 

    public void SendMessage(string message)

    {

        messages.Push(socket.LocalEndPoint.ToString() + ":" +message);

    }

    public void Start()

    {

        //给button绑定回调方法

        connectBtn.onClick.Add(new EventDelegate(Connection));

        sendBtn.onClick.Add(new EventDelegate(Send));

    }

 

    /// <summary>

    ///因为只有主线程才能修改UI组件的属性,因此把它放在Update里面更新

    /// </summary>

    public void Update()

{

if (messages.Count > 0) {

string message=messages.Pop ();

uiTextList.Add (message);

}

}

}

  代码完工后,就开始测试了。

先打开后台服务器,运行起来。因为是控制台程序,运行结果如下:

 Socket网络通信(三)

然后打开客户端程序,我们把它生成.exe文件,然后就可以打开多个了,打开之后就可以测试了。输入服务器IP和端口号,连接服务器。测试结果如下:

 Socket网络通信(三)

我再打开一个客户端,我输入消息,点击发送,看看是否消息同步,测试结果如下:

 Socket网络通信(三)

 Socket网络通信(三)

 

 

 


相关文章:

  • 2019-01-26
  • 2021-11-05
  • 2021-11-05
  • 2021-11-27
  • 2021-05-23
  • 2021-07-06
  • 2018-07-02
  • 2021-09-04
猜你喜欢
  • 2021-11-05
  • 2021-11-05
  • 2021-11-05
  • 2021-05-11
  • 2021-07-24
  • 2021-11-08
  • 2021-11-15
相关资源
相似解决方案