一個簡單地聊天程序

前言:如今微信聊天交友,朋友圈發生活動態算是徹底融入咱們生活一部分了,就連家裏老人都開始玩起了自拍發朋友圈求點贊,對於基本不玩這些的我居然還被她們鄙視了一把。 這段實在是太忙,可貴這個週末空閒,痛定思痛,決定本身實現一個聊天軟件。
先來個簡化版框架,實現了客服端發送消息,而後由服務端廣播,同時將消息記錄寫入數據庫保存。而且客服端能夠經過發送_GET消息從數據庫讀取最近10條記錄廣播。
下面咱們就來分別實現這個聊天軟件的先後端:(數據庫:MySQL,後端:C#,前端:Unity3D)前端

數據庫

這裏先簡單介紹下MySQL數據庫環境配置:1,安裝MySQL數據庫,設置好用戶名和密碼。2,安裝connector,使用C#操做MySQL數據庫時,須要這個MySQL官方提供的鏈接文件。3,程序中引用mysql.data.dll庫。4,安裝Navicat for MySQL,專門操做MySQL數據庫的可視化工具。mysql

下面再來介紹怎麼創建數據庫:
1,打開Navicat for MySQL,點擊文件–>新建鏈接,在彈出的面板中填入IP地址」127.0.0.1」,而後填入用戶名和密碼,點確認按鈕,鏈接本地數據庫。(操做數據庫第一步就是鏈接MySQL,也就是鏈接這個新建的鏈接)
這裏寫圖片描述sql

2,創建msgboard數據庫,用於保存消息。右擊鏈接名,選擇新建數據庫。
這裏寫圖片描述數據庫

3,新建數據表。在msgboard數據庫中新建名爲msg的表,包含id,name和msg3個字段。其中注意id爲自動遞增的int類型。
這裏寫圖片描述後端

這樣咱們簡單聊天程序用於保存消息的數據庫就準備好了。數組

服務端

首先,服務端要處理不少客服端消息,那麼它須要用一個數組來維護全部客服端的鏈接。每一個客服端都有本身的Socket和緩衝區,咱們能夠先定義一個Conn類來表示客服端鏈接,它是服務端程序中的重要數據結構。服務器

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

using System.Net;
using System.Net.Sockets;
using System.Collections;

namespace server
{
    class Conn
    {
        //常量
        public const int BUFFER_SIZE = 1024;
        //Socket
        public Socket socket;
        //是否使用
        public bool isUse = false;
        //Buff
        public byte[] readBuff = new byte[BUFFER_SIZE];
        public int buffCount = 0;
        //構造函數
        public Conn()
        {
            readBuff = new byte[BUFFER_SIZE];
        }
        //初始化
        public void Init(Socket socket)
        {
            this.socket = socket;
            isUse = true;
            buffCount = 0;
        }
        //緩衝區剩餘的字節數
        public int BuffRemain()
        {
            return BUFFER_SIZE - buffCount;
        }
        //獲取客服端地址
        public string GetAdress()
        {
            if (!isUse)
                return "沒法獲取地址";
            return socket.RemoteEndPoint.ToString();
        }
        //關閉
        public void Close()
        {
            if (!isUse)
                return;
            Console.WriteLine("[斷開鏈接]" + GetAdress());
            socket.Close();
            isUse = false;
        }
    }
}

而後咱們來編寫服務端主體結構Serv類,它包含一個Conn類型的對象池,用於維護客服端鏈接。NewIndex方法將找出對象池中還沒有使用的元素下標。
在Start方法中,服務器將經歷Socket,Bind,Listen,而後調用BeginAccept開始異步處理客服端的鏈接。同時調用BeginReceive異步接收消息,並廣播給全部客服端。最後就是將消息保存數據庫。微信

咱們操做MySQL數據庫流程就是:1,先鏈接到MySQL(上面鏈接的IP,端口,用戶名,密碼) 2,選擇數據庫。一個鏈接下能夠有多個不一樣數據庫,因此咱們要指定操做那個數據庫。 3,執行sql語句(如對數據庫裏的表進行增刪改查操做) 4,關閉數據庫數據結構

完整代碼以下:框架

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

using System.Net;
using System.Net.Sockets;

using MySql.Data;
using MySql.Data.MySqlClient;
using System.Data;

namespace server
{
    class Serv
    {
        //監聽套接字
        public Socket listenfd; 
        //客服端鏈接
        public Conn[] conns;
        //最大鏈接數
        public int maxConn = 50;

        //數據庫
        MySqlConnection sqlConn;

        //獲取鏈接池索引,返回負數表示獲取失敗
        public int NewIndex()
        {
            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;
        }

        //開啓服務器
        public void Start(string host, int port)
        {
            //數據庫
            string connStr = "Database=msgboard;Data Source=127.0.0.1;";
            connStr += "User Id=root;Password=chenxiaoxian;port=3306";
            sqlConn = new MySqlConnection(connStr);
            try
            {
                sqlConn.Open();
            }
            catch(Exception e)
            {
                Console.Write("[數據庫]鏈接失敗" + e.Message);
                return;
            }

            //鏈接池
            conns = new Conn[maxConn];
            for(int i = 0; i < maxConn; i++)
            {
                conns[i] = new Conn();
            }
            //Socket
            listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //Bind
            IPAddress ipAdr = IPAddress.Parse(host);
            IPEndPoint ipEp = new IPEndPoint(ipAdr, port);
            listenfd.Bind(ipEp);
            //Listen
            listenfd.Listen(maxConn);
            //Accept
            listenfd.BeginAccept(AcceptCb, null);
            Console.WriteLine("[服務器]啓動成功");
        }


        private void AcceptCb(IAsyncResult ar)
        {
            try
            {
                Socket socket = listenfd.EndAccept(ar);
                int index = NewIndex();

                if(index < 0)
                {
                    socket.Close();
                    Console.WriteLine("[警告]鏈接已滿");
                }
                else
                {
                    Conn conn = conns[index];
                    conn.Init(socket);
                    string adr = conn.GetAdress();
                    Console.WriteLine("客服端鏈接[" + adr + "]conn池ID:" + index);
                    conn.socket.BeginReceive(conn.readBuff, conn.buffCount, conn.BuffRemain(), SocketFlags.None, ReceiveCb, conn);
                }
                //再次調用BeginAccept實現循環
                listenfd.BeginAccept(AcceptCb, null);
            }
            catch(Exception e)
            {
                Console.WriteLine("AcceptCb失敗:" + e.Message);
            }
        }

        private void ReceiveCb(IAsyncResult ar)
        {
            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.readBuff, 0, count);
                Console.WriteLine("收到[" + conn.GetAdress() + "]數據:" + str);

                HandleMsg(conn, 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.readBuff, conn.buffCount, conn.BuffRemain(), SocketFlags.None, ReceiveCb, conn);
            }
            catch(Exception e)
            {
                Console.WriteLine("收到[" + conn.GetAdress() + "]斷開鏈接");
                conn.Close();
            }
        }

        public void HandleMsg(Conn conn, string str)
        {
            //獲取數據
            if(str == "_GET")
            {
                string cmdStr = "select * from msg order by id desc limit 10;";
                MySqlCommand cmd = new MySqlCommand(cmdStr, sqlConn);
                try
                {
                    MySqlDataReader dataReader = cmd.ExecuteReader();
                    str = "";
                    while(dataReader.Read())
                    {
                        str += dataReader["name"] + ":" + dataReader["msg"] + "\n\r";
                    }
                    dataReader.Close();
                    byte[] bytes = System.Text.Encoding.Default.GetBytes(str);
                    conn.socket.Send(bytes);
                }
                catch(Exception e)
                {
                    Console.WriteLine("[數據庫]查詢失敗" + e.Message);
                }
            }
            else
            {
                string cmdStrFormat = "insert into msg set name = '{0}' ,msg = '{1}';";
                string cmdStr = string.Format(cmdStrFormat, conn.GetAdress(), str);
                MySqlCommand cmd = new MySqlCommand(cmdStr, sqlConn);
                try
                {
                    cmd.ExecuteNonQuery();
                }
                catch(Exception e)
                {
                    Console.WriteLine("[數據庫]插入失敗" + e.Message);
                }
            }
        }
    }
}

最後就是在程序main中開啓服務端:

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

using System.Net;
using System.Net.Sockets;

namespace server {
    class Program {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            Serv serv = new Serv();
            serv.Start("127.0.0.1", 1234);

            while(true)
            {
                string str = Console.ReadLine();
                switch(str)
                {
                    case "quit":
                        return;
                }
            }

        }
    }
}

客服端

使用unity製做界面,因爲只是demo版,界面簡單,就不過多介紹了,直接上代碼:

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

public class net : MonoBehaviour {

    //服務器IP和端口
    public InputField hostInput;
    public InputField portInput;
    //顯示客服端收到的消息
    public Text recvText;
    public string recvStr;
    //顯示客服IP和端口
    public Text clientText;
    //聊天輸入框
    public InputField textInput;
    //Socket和接收緩衝區
    Socket socket;
    const int BUFFER_SIZE = 1024;
    public byte[] readBuff = new byte[BUFFER_SIZE];

    //顯示接收消息
    void Update()
    {
        recvText.text = recvStr;
    }

    //鏈接
    public void Connetion()
    {
        //清理text
        recvText.text = "";
        //Socket
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //Connet
        string host = hostInput.text;
        int port = int.Parse(portInput.text);
        socket.Connect(host, port);
        clientText.text = "客服端地址 " + socket.LocalEndPoint.ToString();
        //Recv
        socket.BeginReceive(readBuff, 0, BUFFER_SIZE, SocketFlags.None, ReceiveCb, null);
    }

    //接收回調
    private void ReceiveCb(IAsyncResult ar)
    {
        try
        {
            //cout 是接收數據的大小
            int count = socket.EndReceive(ar);
            //數據處理
            string str = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);
            if (recvStr.Length > 300)
                recvStr = "";
            recvStr += str + "\n";
            //繼續接收
            socket.BeginReceive(readBuff, 0, BUFFER_SIZE, SocketFlags.None, ReceiveCb, null);
        }
        catch(Exception e)
        {
            recvText.text += "鏈接已斷開";
            socket.Close();
        }
    }

    //發送數據
    public void Send()
    {
        string str = textInput.text;
        byte[] bytes = System.Text.Encoding.Default.GetBytes(str);
        try
        {
            socket.Send(bytes);
        }
        catch { }
    }

}

最後來看看咱們的demo示意圖:
服務器運行日誌:
這裏寫圖片描述
兩客服端發送消息模擬:
這裏寫圖片描述
消息寫入數據庫:
這裏寫圖片描述

OK,完工!~

相關文章
相關標籤/搜索