在之前的基礎上改成多人聊天
服務器
using System;
namespace TalkRoomTCP
{
class Program
{
static void Main(string[] args)
{
new TalkSever().Init();
// 接收一個鍵盤輸入的字符,目的是不讓命令行自動關閉
Console.ReadKey();
}
}
}
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TalkRoomTCP
{
//每一個客戶端的結構
class Client
{
public const ushort Buffer_Length = 1024;
public Socket socket;
public byte[] buffer = new byte[Buffer_Length];
}
class TalkSever
{
//存放每一個客戶端
Dictionary<Socket, Client> clientList = new Dictionary<Socket, Client>();
public void Init()
{
//創(chuàng)建socket using 代替Close 用完不關閉會占用端口
Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
//綁定IP 端口號
//IPAddress.Any:相當于"0.0.0.0"的IP地址偵聽本地所有網絡接口上的客戶端活動 有幾個偵聽幾個
//IPAddress.Broadcast:相當于"255.255.255.255"的IP地址,通常用于Udp的數(shù)據(jù)包廣播。
//IPAddress.Loopback:相當于"127.0.0.1"的IP地址,用于指代本機。監(jiān)聽"127.0.0.1"時,只能從本機連接到服務端。
socket.Bind(new IPEndPoint(IPAddress.Any, 9999));
//開啟監(jiān)聽 參數(shù)是最大接受隊列的長度 多于這個就只響應100個 其他拒絕
socket.Listen(100);
//開啟異步 第二個參數(shù)用于傳遞一些數(shù)據(jù)
socket.BeginAccept(AcceptCallBack, socket);
Console.WriteLine("服務器啟動");
}
private void AcceptCallBack(IAsyncResult ar)
{
var socket = ar.AsyncState as Socket;
var clientSocket = socket.EndAccept(ar);
Console.WriteLine($"{clientSocket.RemoteEndPoint}客戶端連接");
Client client = new Client();
client.socket = clientSocket;
clientList.Add(clientSocket, client);
//客戶端接收消息 如果客戶端不發(fā)送數(shù)據(jù) 服務器程序阻塞(掛起)這個位置
var buffer = client.buffer;
clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallBack, client);
// 遞歸繼續(xù)Accept
socket.BeginAccept(AcceptCallBack, socket);
}
private void ReceiveCallBack(IAsyncResult ar)
{
//
Client client = ar.AsyncState as Client;
Socket clientSocket = client.socket;
byte[] buffer = client.buffer;
try
{
int length = clientSocket.EndReceive(ar);
//小于0客戶端就關閉了
if (length > 0)
{
Console.WriteLine($"接收到客戶端的消息:{Encoding.UTF8.GetString(buffer, 0, length)}");
foreach (var item in clientList)
{
item.Key.Send(buffer, length, SocketFlags.None);
}
//遞歸重新開始接收
clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallBack, client);
}
else
{
OnClientDisconnect(clientSocket);
}
}
catch (SocketException ex)
{
// 如果服務端有向客戶端A未發(fā)送完的數(shù)據(jù),客戶端A主動斷開時會觸發(fā)10054異常,在此捕捉
if (ex.SocketErrorCode == SocketError.ConnectionReset)
OnClientDisconnect(clientSocket);
}
}
private void OnClientDisconnect(Socket clientSocket)
{
Console.WriteLine($"{clientSocket.RemoteEndPoint}斷開連接");
clientList.Remove(clientSocket);
clientSocket.Close();
}
}
}

客戶端
然后改造客戶端 客戶端不是實時的 而且順序有問題
先解決 不能實時刷新的問題
還有會有時候接收到連在一起的字符 那是粘包問題 后面改造
主要是我們點擊時候發(fā)送又輸入到屏幕上
其實應該是 點擊后 發(fā)給服務器等服務器返回才輸入屏幕上
/**
*Copyright(C) 2019 by #COMPANY#
*All rights reserved.
*FileName: #SCRIPTFULLNAME#
*Author: #AUTHOR#
*Version: #VERSION#
*UnityVersion:#UNITYVERSION#
*Date: #DATE#
*Description:
*History:
*/
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
public class TalkClient : MonoBehaviour
{
public InputField input;
public Text text;
public Button btn;
byte[] buffer = new byte[1024];
Socket socket;
List<string> msg = new List<string>();
// Start is called before the first frame update
void Start()
{
socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
//連接服務器
socket.Connect("127.0.0.1", 9999);
socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
btn.onClick.AddListener(() =>
{
//發(fā)送數(shù)據(jù)
socket.Send(Encoding.UTF8.GetBytes(input.text));
});
}
private void Update()
{
if (msg.Count>0)
{
foreach (var item in msg)
{
//因為unity不能在子線程調用unity大部分API Debug.Log可以 socket內部異步為我們開了線程
//UniRx插件中有一個MainThreadDispatcher類,可以很方便地用來處理子線程到主線程的轉換
text.text += item + "\n";
}
//清除處理過的消息
msg.Clear();
}
}
void ReceiveCallback(IAsyncResult ar)
{
try
{
//接受數(shù)據(jù)
int length = socket.EndReceive(ar);
if (length > 0)
{
var str = Encoding.UTF8.GetString(buffer, 0, length);
Debug.Log($"接收到服務端的消息:{str}");
msg.Add(str);
//重新開始接受
socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
}
else
{
OnClientDisconnect();
}
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.ConnectionReset)
OnClientDisconnect();
}
}
void OnClientDisconnect()
{
Debug.Log("與服務端斷開連接");
socket.Close();
}
}
然后就OK了

區(qū)分玩家 解決粘包
這樣不知道哪個消息是誰發(fā)的
有三種解決方法
1、 加入注冊登錄功能
2、 在連接服務端成功后先給服務端發(fā)送消息設置昵稱
3、 在每次發(fā)送消息的時候發(fā)送昵稱
可以設計個數(shù)據(jù)包
用名字:信息
上面的放名字

這里一改就行
btn.onClick.AddListener(() =>
{
//發(fā)送數(shù)據(jù)
socket.Send(Encoding.UTF8.GetBytes(inputName.text+":"+input.text));
});
然后是粘包
服務器壓力大的時候會出現(xiàn)
發(fā)送5次然后返回一次 黏在一起
原因 TCP是個"流"協(xié)議,所謂流,就是沒有界限的一串數(shù)據(jù)
會有以下4種情況 234都是粘包
先接收到data1,然后接收到data2。
先接收到data1的部分數(shù)據(jù),然后接收到data1余下的部分以及data2的全部。
先接收到了data1的全部數(shù)據(jù)和data2的部分數(shù)據(jù),然后接收到了data2的余下的數(shù)據(jù)。
一次性接收到了data1和data2的全部數(shù)據(jù)。
相比UDP UDP是個數(shù)據(jù)包協(xié)議 他要么完整要么全丟
服務器客戶端都可能發(fā)生粘包
1、由Nagle算法造成的發(fā)送端的粘包
我們提交一段數(shù)據(jù)給TCP發(fā)送時,TCP并不立刻發(fā)送此段數(shù)據(jù),而是等待一小段時間,看看在等待期間是否還有要發(fā)送的數(shù)據(jù),若有則會一次把這兩段數(shù)據(jù)發(fā)送出去
2.接收端接收不及TCP緩存區(qū)緩存了多個數(shù)據(jù)時造成的接收端粘包
解決方法 封包拆包

封包
1.數(shù)據(jù)轉為json字符串
2.把json轉為byte[]數(shù)組A
3.創(chuàng)建一個長度為數(shù)組A長度+2字節(jié)的字節(jié)數(shù)組B,依次將2字節(jié)的長度和json字節(jié)數(shù)組A先后輸入寫入到這個B中
4.將這個字節(jié)數(shù)組B(數(shù)據(jù)包)發(fā)送給服務端
拆包
1.將最新數(shù)據(jù)放入DataCache
2、嘗試從DataCache中解析數(shù)據(jù)包,具體代碼見上面的Decode
3、一直嘗試解析,直到數(shù)據(jù)不足

一般來說XML json會很大 一般用自定義的二進制格式
數(shù)據(jù)一般分為這兩種 定長的數(shù)據(jù)和不定長的數(shù)據(jù)
定長的數(shù)據(jù)比如:byte,short,int,long,char之類的簡單數(shù)據(jù),以及僅包含這些類型的類或結構體
不定長的數(shù)據(jù)比如:字符串string、列表List、字典Dictionary等等,這些都需要進行特殊處理,一般是在數(shù)據(jù)內容的開頭加上一個長度數(shù)據(jù)。比如寫入一個字符串string時,先寫入2字節(jié)的string的長度,再寫入string的具體內容,類似我們上面處理的json字符串。
比如現(xiàn)在我們的數(shù)據(jù)要改二進制的話

然后谷歌出了個protobuf比較好用 也不用自己寫這么多處理