【C#服务器】(一)基础的服务器逻辑:创建、捆绑、监听、接收、传送

服务器启动-Main方法启动(应用程序窗口主函数启动) 

using GameServer.SocketFile;
using System;

namespace GameServer
{
    class Program
    {
        static void Main(string[] args)
        {        
            Server server = new Server("127.0.0.1", 6688);
            server.Start();
            Console.ReadKey();
        }
    }
}

Server类:

功能:
1、保存和管理所连接的客户端Client类 
2、启动一个服务器Socket,数据流TCP协议沟通,捆绑IP:127.0.0.1,端口: 6688,并允许监听无限量个客户端(有硬件限制)
3、进入异步监听客户端连接,调用Socket.BeginAccept方法,传入接收到客户端连接回调void AcceptCallBack(IAsyncResult ar)
回到中的IAsyncResult是一个异步等待条件和连接结果数据, Socket clientSocket = serverSocket.EndAccept(ar); 这句代码的EndAccept方法会一直等待,直到有客户端连接才会返回Socket数据(即clientSocket),接着才会继续执行代码,再次进入到服务器异步监听客户端连接,形成递归异步监听。
4、客户端连接上服务器后,Server类保存客户端的Socket对象,并执行Client类的Start方法,开始运行客户端Socket的操作。

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using GameServer.Protocol;
namespace GameServer.SocketFile
{
    public class Server
    {
        private IPEndPoint ipEndPoint;
        private Socket serverSocket;
        private List<Client> clientList = new List<Client>();
        public Server() { }
        public Server(string ipStr, int port)
        {
            ipEndPoint = new IPEndPoint(IPAddress.Parse(ipStr), port);
            ProtocolMgr.Instance.Init();
        }
        ~Server()
        {
        }
        public void Start()
        {
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            Console.WriteLine("正在捆绑IP和端口号... ...");
            //捆绑IP和端口
            serverSocket.Bind(ipEndPoint);
            Console.WriteLine("捆绑IP和端口号完毕");
            //监听无数个客户端连接
            serverSocket.Listen(0);
            Console.WriteLine("开启监听客户端连接");
            serverSocket.BeginAccept(AcceptCallBack, null);
        }
        /// <summary>
        /// 异步接收客户端里阿尼额
        /// </summary>
        /// <param name="ar"></param>
        private void AcceptCallBack(IAsyncResult ar)
        {
            Console.WriteLine("正在接收客户端连接... ...");
            Socket clientSocket = serverSocket.EndAccept(ar);
            Console.WriteLine("接收到一条客户端连接" + ar.ToString());
            Client client = new Client(clientSocket, this);
            clientList.Add(client);
            //该客户端socket开启监听客户端消息
            client.Start();
            //继续接收下一个客户端连接
            serverSocket.BeginAccept(AcceptCallBack, null);
        }
        public void RemoveClient(Client client)
        {
            lock (clientList)
            {
                clientList.Remove(client);
            }
        }
    }
}

 Client类:

功能:
1、持有自身的客户端Socket和管理它的服务器Socket
2、使用自身客户端Socket对象开启线程接收服务器数据。 

clientSocket.BeginReceive(msg.Data, msg.StartIndex, msg.RemainSize, SocketFlags.None, ReceiveCallBack, null);
这里很关键,参数解释如下:
2.1、 首先msg是一个Message类对象,Data是byte[1024]数组,StartIndex是Data数组的[首个未填数据索引号],RemainSize是Data数组的可填充个数,ReceiveCallBack是接收到服务器数据回调方法,其他参数暂时没了解。
2.2  ReceiveCallBack讲解: int count = clientSocket.EndReceive(ar); 与监听客户端连接时同理,一样会等待有服务器数据到来才会返回count并继续执行,count是服务器数据传递过来的字节个数,服务器传递的是字节数据。
如果接收到的数据是0个,说明已断开服务器连接,会自动关闭这个客户端Socket(调用Close方法)
2.3 如果正常接收到数据,数据个数不为0的情况则为正常,服务器数据会由BeginReceive方法自动装入msg.Data字节数组中,并且是追加形式装入,不会覆盖掉原本数组还存在的数据。
2.4  msg.ReadMessage(count); 通过这个方法传入count来处理Data数据,具体如何处理下面继续解释。

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

namespace GameServer.SocketFile
{
    public class Client
    {
        private Server server;
        private Socket clientSocket;
        private Message msg;
        public Client()
        {
        }
        public Client(Socket clientSocket, Server server)
        {
            this.clientSocket = clientSocket;
            this.server = server;
            msg = new Message(this);
        }

        /// <summary>
        /// 监听客户端消息
        /// </summary>
        public void Start()
        {
            if (clientSocket != null && clientSocket.Connected == true)
            {
                //开启一个线程去接收
                clientSocket.BeginReceive(msg.Data, msg.StartIndex, msg.RemainSize, SocketFlags.None, ReceiveCallBack, null);
            }
        }

        private void ReceiveCallBack(IAsyncResult ar)
        {
            try
            {
                if (clientSocket == null || clientSocket.Connected == false) return;
                //接收数据 count为整个包的长度
                int count = clientSocket.EndReceive(ar);
                if (count == 0)
                {
                    Close();
                }
                //处理数据
                msg.ReadMessage(count);
                //继续监听客户端消息
                Start();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Close();
            }
        }

        /// <summary>
        /// 发送响应给客户端
        /// </summary>
        public void SendResponse(byte[] datas)
        {
            if (clientSocket != null && clientSocket.Connected == true)
            {
                //需要拼接上数据长度作为包头
                byte[] dataCountBytes = BitConverter.GetBytes(datas.Length);
                clientSocket.Send(dataCountBytes.Concat(datas).ToArray());
            }
            else
            {
                Console.WriteLine("服务器已中断");
            }
        }

        private void Close()
        {
            if (clientSocket != null)
            {
                clientSocket.Close();
            }
            if (server != null)
            {
                server.RemoveClient(this);
            }
        }
    }
}

Message类: 

功能:
1、接收服务器传递来的数据,保存在Data数组 byte[1024],接收的数据是追加在数组可填充位置上,且是连续的。
2、处理每一个数据包(服务器发送的是一个个数据包)时,确保数据包的字节数据已经完整地存储到Data数组上
3、每一个数据包会传入一个万能的处理数据包函数,根据主协议号和子协议号

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GameServer.Protocol;
using System.Net.Sockets;
using GameServer.ToolUtility;
using GameServer.Protocol.Data;

namespace GameServer.SocketFile
{
    class Message
    {
        private Client client;
        private int startIndex;
        private List<byte> tempList = new List<byte>();
        private byte[] data = new byte[1024];
        public byte[] Data
        {
            get { return data; }
            set { data = value; }
        }
        public int StartIndex
        {
            get { return startIndex; }
            set { startIndex = value; }
        }
        public int RemainSize
        {
            get
            {
                return data.Length - startIndex;
            }
        }

        public Message(Client client)
        {
            this.client = client;
        }

        /// <summary>
        /// 处理服务器接收客户端数据的解包
        /// </summary>
        /// <param name="count"></param>
        public void ReadMessage(int count)
        {
            startIndex += count;
            while (true)
            {
                //通用解包和发包                
                if (startIndex > 12)
                {
                    //仅有没有缓存情况才考虑完整数据
                    if (tempList.Count <= 0)
                    {
                        int dataCount = BitConverter.ToInt32(data, 0);
                        //1.数据完整情况
                        if (startIndex - 4 >= dataCount)
                        {
                            ProtocolEventType type = ProtocolEventType.Null;
                            object o = ProtocolMgr.Instance.StartUnPack(ref data, ref type);
                            if (type != ProtocolEventType.Null)
                            {
                                EventSystem.Instance.Dispatch(type, o, client);
                            }
                            startIndex -= (4 + dataCount);
                        }
                        else//2.数据不完整情况,存[0,startIndex-1]字节缓存list
                        {
                            for (int i = 0; i < startIndex; i++)
                            {
                                tempList.Add(data[i]);
                            }
                            //出现数据不完整情况,就必定是数据不完整是最后面的一组协议,故后面应该是没有任何字节的了
                            Array.Clear(data, 0, data.Length);//全清(可能会引发bug)
                            startIndex = 0;
                        }
                    }
                    else//3.肯定是缓存的数据后续部分(一个协议后续部分数据)到来的情况,要继续获取那一部分数据
                    {
                        HandleTempList();
                    }
                }
                else
                {
                    //1.协议异常
                    //2.有缓存的情况下,可能是缓存的相关数据到来                    (目前仅考虑)
                    if (tempList.Count > 0)
                    {
                        HandleTempList();
                    }
                    else
                    {
                        //协议异常直接退出
                        return;
                    }
                }
            }
        }
        private void HandleTempList()
        {
            //现有缓存数据长度
            int curLength = tempList.Count - 4;
            //总数据长度 = 保存于缓存[0,3]字节
            int sumLength = BitConverter.ToInt32(tempList.ToArray(), 0);
            //所需数据长度
            int needLength = sumLength - curLength;
            //可取长度,若当前网络对象缓存字节数量 小于 所需数据长度,那就拿当前网络对象的所有数据,
            //如果当前网络对象缓存字节数量 大于等级 所需数据长度,那就拿所需数据长度
            int canGetLength = Math.Min(this.startIndex, needLength);
            for (int i = 0; i < canGetLength; i++)
            {
                tempList.Add(data[i]);
            }
            //去除已取出部分字节
            Tool.ByteSub(ref data, canGetLength);
            this.startIndex -= canGetLength;
            curLength = tempList.Count - 4;
            //获取到完整数据开始解析(正常情况下,肯定是完整了)
            if (sumLength - curLength <= 0)
            {
                ProtocolEventType type = ProtocolEventType.Null;
                byte[] bytesArr = tempList.ToArray();
                object o = ProtocolMgr.Instance.StartUnPack(ref bytesArr, ref type);
                if (type != ProtocolEventType.Null)
                {
                    EventSystem.Instance.Dispatch(type, o, client);
                }
                //清空缓存
                tempList.Clear();
            }
        }
    }
}

后面的我懒得介绍了,有点复杂,工程源码在下方链接,可自己选看,基本上以上说明已经结束了

https://github.com/AMikeW/CSharpSocketProtocolTool

(注意使用前先将IP改为自己的IP或127.0.0.1) 本软件是使用套接字、ReceiveCallBack(IAsyncResult AR)函数为例的客服实例,修正了关闭客户端会导致异常的Bug;并且还是一个RichTextBox颜色使用的范例,不同的事件使用不同的颜色:如用户登录用红色、用户名用绿色、聊天内容用黑色^_^! 代码附赠全套注释,帮助初学者学习使用。 下面是核心代码 private void ReceiveCallBack(IAsyncResult AR) { try { DateTime dt = DateTime.Now; //如果服务器突然关闭后,客户端还坚持与之连接就会弹出异常; //检查是否套接字还连接上就可以避免这一问题。 if (!ClientSocket.Connected) { return; } //挂起AR,独占的使用AR来接收传过来的内容 int REnd = ClientSocket.EndReceive(AR); string StrOfREnd=Encoding.Unicode.GetString(MsgBuffer, 0, REnd); //截断的传输过来的字符串,"\n"前的是用户名 "\n"后的是聊天的内容 string UsersName = StrOfREnd.Substring(0, StrOfREnd.LastIndexOf("\n")); string Content = StrOfREnd.Substring(StrOfREnd.LastIndexOf("\n")+1); string Login=StrOfREnd.Substring(0,2); //MessageBox.Show("缓存中的内容:" + StrOfREnd + "\n" + "截断的用户名:" + UsersName + "\n" + "截断的内容:" + Content); if (Login != "登录") { //第一个字符不为“登陆” int oldlenth = tb_RecieveMsg.TextLength; this.tb_RecieveMsg.Select(oldlenth, 0); this.tb_RecieveMsg.SelectionColor = Color.Green; string str = Encoding.Unicode.GetString(MsgBuffer, 0, REnd); str = str.Substring(1, str.Length - 1); //用户使用绿色字体 this.tb_RecieveMsg.AppendText(" " + string.Format("{0:T}", dt) + " " + "用户:" + UsersName + "说:" + "\r\n"); this.tb_RecieveMsg.SelectionColor = Color.Black; this.tb_RecieveMsg.AppendText(" " + Content + "\r\n"); this.tb_RecieveMsg.AppendText("\r\n"); } else {
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值