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