Unity3D网络游戏实战——正确收发数据流

前言

本章主要介绍和实现怎样正确和高效地处理TCP数据(数据流)。也解决了上一章我们遇到的一些问题

4.1TCP数据流

4.1.1系统缓冲区

收到对端数据时,操作系统会将数据存入到Socket的接收缓冲区中,而且操作系统层面上的缓冲区完全由操作系统操作,程序并不能直接操作,只能通过socket.Receive、socket.Send方法来间接操作。
注意:之后出现的readBuff不是发送缓冲区也不是接收缓冲区,而是用户自定义的缓冲区,用于存放两个操作系统缓冲区读取出的字节数据。

4.1.2粘包半包现象

粘包:
有时候你想发送两条数据“童立华”和“好帅”,期望其他客户端分别展示这两条数据。但是Receive可能把两条信息当做一条处理,最后显示出来的是“童立华好帅”。这就是粘包现象,因为Receive返回操作系统接收缓冲区中存放的内容。
半包:
当你想发送“童立华颜晓仪是猪”,但是接收端调用Receive的时候,只接收到了“童立华”,等待一小段时间后才接收到了“颜晓仪是猪”。最后就分开显示“童立华”,“颜晓仪是猪”两条数据。
因为TCP是基于流的数据,粘包很正常。但是直觉告诉我们:一次发送多少数据,一次就接收多少数据才正常。

在服务端Accept后用

System.Threading.Thread.Sleep(30*1000);

,并在此期间让客户端多次发送数据,就可以人工复现粘包。

4.3解决粘包问题

  • 长度信息法:在每个数据包前加上长度信息,每次收到数据后,先读取表示长度的字节,然后从缓冲区取出相应长度的字节。Int16的范围是0-65535,一般一条消息的长度用Int16就可以了。
  • 固定长度法:每次读取固定长度的信息,如果有超出的,就取出然后等下次接收信息后拼接。
  • 结束符号法:可以用一个结束符号作为消息间的分隔符。当读取到结束符时如果还有消息,就取出等下次接收信息后拼接。

4.3.1发送数据

如果要发送HelloWorld,用长度信息法来解决。最后发送的就是“0AHelloWorld”。用Linq命名空间下的Concat方法来拼接长度数组和信息数组后发送即可。代码最后呈上。

4.3.2接收数据

核心思想是定义一个缓冲区readBuff和记录缓冲区现在有多少数据的长度变量buffCount。如果缓冲区有未处理的数据,就把新读的数据放在有效数据之后。

4.3.3处理数据

如果缓冲区数据足够长,超过一条消息的长度,就把消息提取出来处理。
如果数据长度不够,就不去处理它,等待下一次接受数据。

  • 缓冲区长度小于等于2,那就是不够将长度信息解析出来,就等到下一次接受数据。
  • 缓冲区长度大于2,但不足以组成一条消息的时候,比如05hell,就不去处理它,等待下一次接收。
  • 缓冲区长度大于等于一条完整消息,就解析出来,然后更新缓冲区,也就是用array.copy()函数将后面的移到前面,因为解析完的缓冲区数据已经没用了。

4.4大端小端问题

粘包半包的问题占据了收发数据问题的80%,大端小端问题就是剩余的20%其中之一。
我们是用

BitConverter.ToInt16()

来将长度标记字节转为Int16的,但是看了它的源码会发现,根据计算机是大端还是小端编码,计算编码方式会有不同。
那么对于不同的计算机,读取出来的数据长度也会有不同!

4.4.1为什么有大端小端之分

总而言之是个历史问题,我就不多赘述了!

4.4.2用Reverse()兼容大小端编码

我们规定使用小端编码,就判断系统是否是小端编码的系统,如果不是就用Reverse()将大端编码转为小端。

if(!BitConverter.IsLittleEndian){
	lenBytes.Reverse();
}

所以接下来我们都是手动还原前两位数字为Int16,用小端编码的还原形式

Int16 bodyLen = (short)((readBuff[1] << 8) | readBuff[0]);

此处的“|”是逻辑与,等同于位相加。

4.5完整发送数据

简单假设我们操作系统缓冲区为8字节

  • 先发送04hero,剩下2字节,网络不好,没法送出去,继续在操作系统的发送缓冲区中
  • 再发送03cat,cat写不下,03被写入,被成功发送后,缓冲区为空
  • 再发送02hi
  • 服务器解析时,0302h就被解析出来了,导致出错

4.5.2如何解决发送不完整问题

在发送前将数据保存起来,如果不完整,在Send回调函数中继续发送没发完的数据,直到把新的数据发完才能让新的数据进入发送缓冲区
防止在调用BeginSend调用回调这个时间段内再次点击发送出现问题,我们用一个写入队列是否为空来判断,**每次只能有一条数据!!!**如果还没发干净,你再点击send也不会把新数据加进缓冲区

4.5.3ByteArray和Queue

ByteArray是什么?因为我们要发送没发完的数据,所以就用ByteArray来封装byte[]、readIdx和length,这样才可以用readIdx作为下标记录上一次发送到哪里,直接取出byte[]对应的位数发送即可。
Queue的好处在于入队出队为O(1),如果是一个数组,那么就需要O(n)的时间复杂度来移动。

4.5.4解决线程冲突

由异步机制可以知道,BeginSend和回调函数执行于不同县城,如果同时操作writeQueue就会出错。比如第二次发送时,第一次发送的回调函数刚好被调用,当第一次出完队之后,再次判断是否要发送,此时第二次发送的东西刚好入队,那么第一次以为是自己没发干净的东西,再发一次。那么第二次又发一次,就将第二次的数据发送了两次。
为了避免线程竞争,可以通过加锁lock的方式处理。当两个线程争夺一个锁的时候,一个线程等待,被组织的那个锁变为可用。

4.6高效的接收数据

4.6.1不足之处

1.Copy操作
O(n)的时间复杂度当然达咩,所以我们用一个ByteArray数组来作为缓冲区,使用readIdx指向缓冲区的第一个数据,每次解析数据后,就将readIdx增加。比如解析了"03cat",就+5,当缓冲区长度不够时,做一次Array.Copy即可。非常舒服。

2.缓冲区不够长
如果网络不好,就会把缓冲区撑爆。当长度不够时,让它自动扩展,重新申请一个较长的bytes数组。

4.6.2完整的ByteArray

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class ByteArray
{
    //默认大小
    const int DEFAULT_SIZE = 1024;
    //初始大小
    int initSize = 0;
    //用户缓冲区
    public byte[] bytes;
    //读写位置
    public int readIdx = 0;
    public int writeIdx = 0;
    //容量
    private int capacity = 0;
    //剩余空间
    public int remain { get { return capacity - writeIdx; } }
    //数据长度
    public int length { get { return writeIdx - readIdx; } }

    //构造函数
    public ByteArray(byte[] defaultBytes)
    {
        bytes = defaultBytes;
        capacity = defaultBytes.Length;
        initSize = defaultBytes.Length;
        readIdx = 0;
        writeIdx = defaultBytes.Length;
    }

    //构造函数
    public ByteArray(int size = DEFAULT_SIZE)
    {
        bytes = new byte[size];
        capacity = size;
        initSize = size;
        readIdx = 0;
        writeIdx = 0;
    }

    public override string ToString()
    {
        return BitConverter.ToString(bytes, readIdx, length);
    }

    public string Debug()
    {
        return string.Format("readIdx({0}) writeIdx({1}) bytes({2}))", readIdx, writeIdx, BitConverter.ToString(bytes, 0, bytes.Length));
    }

    public void Resize(int size)
    {
        if (size < length) return;
        if (size < initSize) return;
        int n = 1;
        //n是2的倍数,但是大于size!
        while (n < size) n *= 2;
        capacity = n;
        byte[] newBytes = new byte[capacity];
        Array.Copy(bytes, readIdx, newBytes, 0, writeIdx - readIdx);
        bytes = newBytes;
        writeIdx = length;
        readIdx = 0;
    }

    //检查并移动数据,,多点remain,避免bytes过长
    public void CheckAndMoveBytes()
    {
        if(length < 8)
        {
            MoveBytes();
        }
    }
    //移动数据
    public void MoveBytes()
    {
        if(length > 0)
        {
            Array.Copy(bytes, readIdx, bytes, 0, length);
        }
        writeIdx = length;
        readIdx = 0;
    }

    //写入数据
    public int Write(byte[] bs,int offst,int count)
    {
        if(remain < count)
        {
            Resize(length + count);
        }
        Array.Copy(bs, offst, bytes, writeIdx, count);
        writeIdx += count;
        return count;
    }

    //读取数据
    public int Read(byte[] bs, int offset, int count)
    {
        count = Math.Min(count, length);
        Array.Copy(bytes, readIdx, bs, offset, count);
        readIdx += count;
        CheckAndMoveBytes();
        return count;
    }

    //读取Int16
    public Int16 ReadInt16()
    {
        if (length < 2) return 0;
        Int16 ret = (Int16)((bytes[readIdx + 1] << 8) | bytes[readIdx]);//手动还原
        readIdx += 2;
        CheckAndMoveBytes();
        return ret;
    }

    //读取32
    public Int32 ReadInt32()
    {
        if (length < 4) return 0;
        Int32 ret = (Int32)((bytes[readIdx + 3] << 24) |
                            (bytes[readIdx + 2] << 16) |
                            (bytes[readIdx + 1] << 8) |
                            (bytes[readIdx + 0]));
        readIdx += 4;//4位读取完毕,移动下标
        CheckAndMoveBytes();
        return ret;
    }
}

4.7客户端代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using UnityEngine.UI;
using System;
using System.Linq;

public class Echo : MonoBehaviour
{
    //定义套接字
    Socket socket;
    //UGUI
    public InputField InputField;
    public Text text;

    //接收缓冲区
    byte[] readBuff = new byte[1024];
    //最新的接收缓冲区
    ByteArray readBuff1 = new ByteArray();

    //接收缓冲区的数据长度
    int buffCount = 0;
    //显示文字
    string recvStr = "";

    bool canSend = false;

    List<Socket> checkRead = new List<Socket>();

    //定义发送缓冲区
    byte[] sendBytes = new byte[1024];
    //缓冲区偏移值
    int readIdx = 0;
    //缓冲区剩余长度
    int length = 0;

    Queue<ByteArray> writeQueue = new Queue<ByteArray>();

    //点击连接按钮
    public void Connection()
    {
        //新建sockect
        //地址族IPV4,套接字类型stream,协议类型
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //connect
        //远程IP地址,远程端口,阻塞方法,卡住直到服务端回应、
        //自己建立服务器的话,ip地址和端口号就是这两个
        socket.Connect("127.0.0.1", 8888);
        //如果是异步就在callback里接收,同步就直接接收
        socket.BeginReceive(readBuff1.bytes, readBuff1.writeIdx, readBuff1.remain, 0, ReceiveCallback, socket);

        //异步
        //socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);
    }

    //Connect回调
    public void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            //此socket可由ar.AsyncState获取到
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("Socket Connect Succ");

            //接收缓冲区、0表示从readBuff第0位开始接收数据(和TCP粘包问题有关)、每次最多接收1024字节数据(即使服务器发送1025,也只接收1024)
            //接收函数调用时机:在连接成功后就开始接受数据,接收到数据后,回调函数ReceiveCallback被调用
            socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Connect fail" + ex.ToString());
        }
    }

    //Receive回调
    public void ReceiveCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            //获取接收到的数据长度,并更新缓冲区的数据长度
            int count = socket.EndReceive(ar);
            readBuff1.writeIdx += count;
            //处理二进制消息
            OnReceiveData();
            //用了专门的处理函数就不需要了
            //string s = System.Text.Encoding.Default.GetString(readBuff, 0, count);
            //recvStr = s + "\n" + recvStr;
            //等待,模拟粘包
            //System.Threading.Thread.Sleep(1000 * 30);
            //接收完一串数据后,等待下一串数据的到来
            if(readBuff1.remain < 8)
            {
                readBuff1.MoveBytes();
                readBuff1.Resize(readBuff1.length * 2);
            }
            socket.BeginReceive(readBuff1.bytes, readBuff1.writeIdx, readBuff1.remain, 0, ReceiveCallback, socket);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Receive fail" + ex.ToString());
        }
    }

    public void OnReceiveData()
    {
        Debug.Log("[Recv 1] buffCount=" + readBuff1.length);
        Debug.Log("[Recv 2] readbuff=" + readBuff1.ToString());
        //消息长度
        if (readBuff1.length <= 2) return;
        int readIdx = readBuff1.readIdx;
        byte[] bytes = readBuff1.bytes;
        Int16 bodyLength = (Int16)((bytes[readIdx + 1] << 8)|bytes[readIdx]);//手动以小端形式来还原
        if (readBuff1.length < 2 + bodyLength) return;
        readBuff1.readIdx += 2;
        Debug.Log("[Recv 3] bodyLength=" + bodyLength);
        //消息体
        byte[] stringByte = new byte[bodyLength];
        readBuff1.Read(stringByte, 0, bodyLength);
        string s = System.Text.Encoding.UTF8.GetString(stringByte);
        Debug.Log("[Recv 4] s=" + s);
        //更新缓冲区
        //int start = 2 + bodyLength;
        //int count = buffCount - start;
        //Array.Copy(readBuff, start, readBuff, 0, count);
        //buffCount -= start;
        Debug.Log("[Recv 5] readbuff=" + readBuff1.ToString());//更新后的buffcount
        //消息处理
        recvStr = s + '\n' + recvStr;
        //继续读取
        if (readBuff1.length > 2)
        {
            OnReceiveData();
        }
    }

    //点击发送按钮
    public void Send()
    {
        //send
        //if (canSend)
        //{
        //    string sendStr = InputField.text;
        //    //string sendStr = System.DateTime.Now.ToString();
        //    //将str转化为字节流
        //    byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        //    //socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);

        //    socket.Send(sendBytes);
        //}
        //异步不需要receive
        //Recv
        //byte[] readBuff = new byte[1024];
        接收数据的长度
        //int count = socket.Receive(readBuff);
        //string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
        //text.text = recvStr;
        Close
        //socket.Close();

        string sendStr = InputField.text;
        //组装协议
        byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        Int16 len = (Int16)bodyBytes.Length;
        byte[] lenBytes = BitConverter.GetBytes(len);
        //大小端编码
        if (!BitConverter.IsLittleEndian)
        {
            Debug.Log("[Send] Reverse lenBytes");
            lenBytes.Reverse();
        }
        byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
        //现在开始用ByteArray来封装这些数据
        //length = sendBytes.Length;//数据长度
        //readIdx = 0;
        方便:同步、不抛异常
        //socket.BeginSend(sendBytes, readIdx, length, 0, SendCallback, socket);
        //Debug.Log("[Send]" + BitConverter.ToString(sendBytes));
        ByteArray ba = new ByteArray(sendBytes);
        int count = 0;
        //加锁,只有一个线程可以操作
        lock (writeQueue)
        {
            writeQueue.Enqueue(ba);
            count = writeQueue.Count;
        }
        //一定要把前面的发完,只剩下当前要发送的才发送
        if(count == 1)
        {
            socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, socket);
        }
    }

    //Send回调
    public void SendCallback(IAsyncResult ar)
    {
        try
        {
            //这个socket是传进回调的用户定义对象,可强转为socket
            Socket socket = (Socket)ar.AsyncState;
            int count = socket.EndSend(ar);
            //判断是否发送完整
            ByteArray ba;
            //加锁!
            lock (writeQueue)
            {
                ba = writeQueue.First();
            }
            //每次加上发送的长度,看看end - start = length是否为0,为0证明发完就可以弹出
            ba.readIdx += count;
            if (ba.length == 0)
            {
                //只要是取first的都要加锁!
                lock (writeQueue)
                {
                    //如果剩余长度为0,证明发送完整了
                    writeQueue.Dequeue();
                    ba = writeQueue.First();
                }
            }
            //但是如果发送不完整,是不是会收到一条完整的和一条不完整的?
            //不是,会先发出一段,再发出后半段。因为readIdx也就是start在变
            if(ba != null)
            {
                //如果发送不完整或者发送完整且存在第二条数据
                socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, socket);
            }
            Debug.Log("Socket Send succ" + count);
        }
        catch(SocketException ex)
        {
            Debug.Log("Socket Send fail" + ex.ToString());
        }
    }


    public void Update()
    {
        //if(socket == null)
        //{
        //    return;
        //}
        poll客户端
        //if (socket.Poll(0, SelectMode.SelectRead))
        //{
        //    byte[] readBuff = new byte[1024];
        //    int count = socket.Receive(readBuff);
        //    //不阻塞模式,microSeconds=0
        //    string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
        //    text.text = recvStr;
        //}

        处理阻塞send应该也差不多
        //if (socket.Poll(0, SelectMode.SelectWrite))
        //{
        //    canSend = true;
        //}
        //else
        //{
        //    canSend = false;
        //}

        select客户端
        只需检测一个socket,将这个socket加入到待监测列表即可
        //checkRead.Clear();
        //checkRead.Add(socket);
        select
        //Socket.Select(checkRead, null, null, 0);
        check
        //foreach (Socket s in checkRead)
        //{
        //    byte[] readBuff = new byte[1024];
        //    int count = socket.Receive(readBuff);
        //    string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
        //    text.text = recvStr;
        //}
        text.text = recvStr;
    }
}

4.8服务端代码

using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;

//hh

public class ClientState
{
    public Socket socket;
    public byte[] readBuff = new byte[1024];
    public int hp = -100;
    public float x = 0;
    public float y = 0;
    public float z = 0;
    public float eulY = 0;
}
class MainClass
{
    //异步服务器
    //监听Socket
    static Socket listenfd;
    //客户端Socket及状态信息
    public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();

    public static void Main(string[] args)
    {
        Console.WriteLine("Hello");
        //Socket
        Socket listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //Bind
        IPAddress ipAdr = IPAddress.Parse("127.0.0.1");//IP地址
        IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);//IP和端口
        //给listenfd套接字绑定IP和端口,"127.0.0.1"地址和8888号端口
        listenfd.Bind(ipEp);
        //Listen,等待客户端连接
        //0表示容纳等待接收的客户端连接数不受限制,1代表最多可容纳等待接受的连接数为1
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");

        //同步服务器
        //while (true)
        //{
        //    //Accept
        //    //接收客户端连接,均为阻塞方法,如果没有客户端连接就不会向下执行
        //    //返回一个新客户端的socket对象,对于服务器来说
        //    //它有一个监听 Socket(listenfd)用来监听和应答客户端的连接,对每个客户端还有专门一个socket(connfd)用于处理客户端的数据
        //    Socket connfd = listenfd.Accept();
        //    Console.WriteLine("[服务器]Accetp");
        //    //Receive
        //    byte[] readBuff = new byte[1024];
        //    int count = connfd.Receive(readBuff);
        //    string readStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
        //    Console.WriteLine("[服务器接收]" + readStr);
        //    //Send
        //    byte[] sendBytes = System.Text.Encoding.Default.GetBytes(readStr);

        //    //如果客户端用异步,即使收不到服务端的消息也不会卡住主线程
        //    connfd.Send(sendBytes);
        //}

        //异步服务器
        //listenfd.BeginAccept(AcceptCallback, listenfd);
        等待
        //Console.ReadLine();

        阻塞poll服务器
        //while (true)
        //{
        //    //检查listenfd,可读就添加客户端信息
        //    if (listenfd.Poll(0, SelectMode.SelectRead))
        //    {
        //        ReadListenfd(listenfd);
        //    }
        //    //检查clientfd
        //    foreach (ClientState s in clients.Values)
        //    {
        //        Socket clientfd = s.socket;
        //        if (clientfd.Poll(0, SelectMode.SelectRead))
        //        {
        //            //false表示客户端断开(收到长度为0的数据)
        //            //断开会删掉列表中对应的信息,导致遍历失败,所以直接break
        //            if (!ReadClientfd(clientfd))
        //            {
        //                break;
        //            }
        //        }
        //    }
        //    //防止CPU占用过高
        //    //让程序挂起1ms,避免死循环让CPU喘息
        //    System.Threading.Thread.Sleep(1);
        //}

        //select服务器
        //checkRead
        List<Socket> checkRead = new List<Socket>();
        //主循环
        while (true)
        {
            //填充checkRead列表
            checkRead.Clear();
            checkRead.Add(listenfd);
            foreach (ClientState s in clients.Values)
            {
                checkRead.Add(s.socket);
            }
            //select,只将待检查可读的列表传进去
            Socket.Select(checkRead, null, null, 1000);
            //调用完上面的方法后这个列表就被改了,这个列表中只有可读的socket
            foreach (Socket s in checkRead)
            {
                //因为listnfd本身就被加进去了
                if (s == listenfd)
                {
                    ReadListenfd(s);
                }
                //除了listen其余都是可读的客户端,直接处理即可
                else
                {
                    ReadClientfd(s);
                }
            }
        }
}

    //读取listenfd,和一步服务端的acceptcallback相似,用于应答客户端,添加客户端信息
    public static void ReadListenfd(Socket listenfd)
    {
        Console.WriteLine("Accept");
        Socket clientfd = listenfd.Accept();
        ClientState state = new ClientState();
        state.socket = clientfd;
        clients.Add(clientfd, state);
    }

    //和异步服务端的Receivecallback类似,用于接收客户端消息,并广播给所有客户端
    public static bool ReadClientfd(Socket clientfd)
    {
        ClientState state = clients[clientfd];
        //接收
        int count = 0;
        try
        {
            count = clientfd.Receive(state.readBuff);
        }
        catch(SocketException ex)
        {
            clientfd.Close();
            clients.Remove(clientfd);
            Console.WriteLine("Receive SocketException" + ex.ToString());
            return false;
        }
        //让客户端关闭
        if(count == 0)
        {
            clientfd.Close();
            clients.Remove(clientfd);
            Console.WriteLine("Socket Close");
            return false;
        }
        //广播
        //enter|和list|一起到了怎么拆分,因为是同一个客户端发过来的,都在readbuff里
        string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 2, count-2);
        Console.WriteLine("Receive" + recvStr);
        byte[] sendBytes = new byte[count];
        Array.Copy(state.readBuff, 0, sendBytes, 0, count);
        foreach (ClientState cs in clients.Values)
        {
            cs.socket.Send(sendBytes);
        }
        return true;
    }

    //Accept回调
    //是beginaccept的回调函数,处理3件事
    //1.给新的连接分配ClientState,并把它加入到clients列表中
    //2.异步接收客户端数据
    //3.再次调用BeginAccept循环
    public static void AcceptCallback(IAsyncResult ar)
    {
        try
        {
            Console.WriteLine("[服务器]Accept");
            //监听和应答客户端的socket
            Socket listenfd = (Socket)ar.AsyncState;
            //处理该客户端的socket
            Socket clientfd = listenfd.EndAccept(ar);
            //clients列表
            ClientState state = new ClientState();
            //初始化此客户端类,key和value岂不是重复利用了?
            state.socket = clientfd;
            clients.Add(clientfd, state);
            //接收数据BeginReceive,以ClientState取代Socket
            clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state);
            //继续Accept
            listenfd.BeginAccept(AcceptCallback, listenfd);
        }
        catch(SocketException ex)
        {
            Console.WriteLine("Socket Accept fail" + ex.ToString());
        }
    }

    //Receive回调
    //1.服务端收到消息后,回应客户端
    //2.如果收到客户端关闭连接的信号"if(count==0)",断开连接
    //3.继续调用BeginReceive接收下一个数据
    public static void ReceiveCallback(IAsyncResult ar)
    {
        try
        {
            //发送消息的客户端
            ClientState state = (ClientState)ar.AsyncState;
            Socket clientfd = state.socket;
            //当receive返回值小于等于0时,表示socket连接可以断开
            int count = clientfd.EndReceive(ar);
            //客户端关闭
            if(count == 0)
            {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return;
            }
            //从收到的字节流转为string
            string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 0, count);
            string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);//string转为bytes
            //用于处理该客户端数据的socket
            foreach(ClientState s in clients.Values)
            {
                s.socket.Send(sendBytes);
            }
            clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state);
        }
        catch(SocketException ex)

        {
            Console.WriteLine("Socket Receive fail" + ex.ToString());
        }
    }

    public static void Send(ClientState cs,string sendStr)
    {
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        cs.socket.Send(sendBytes);
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值