在之前的基础上改成多人聊天
服务器
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 clientList = new Dictionary();
public void Init()
{
//创建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的数据包广播。
//IPAddress.Loopback:相当于"127.0.0.1"的IP地址,用于指代本机。监听"127.0.0.1"时,只能从本机连接到服务端。
socket.Bind(new IPEndPoint(IPAddress.Any, 9999));
//开启监听 参数是最大接受队列的长度 多于这个就只响应100个 其他拒绝
socket.Listen(100);
//开启异步 第二个参数用于传递一些数据
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);
//客户端接收消息 如果客户端不发送数据 服务器程序阻塞(挂起)这个位置
var buffer = client.buffer;
clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallBack, client);
// 递归继续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未发送完的数据,客户端A主动断开时会触发10054异常,在此捕捉
if (ex.SocketErrorCode == SocketError.ConnectionReset)
OnClientDisconnect(clientSocket);
}
}
private void OnClientDisconnect(Socket clientSocket)
{
Console.WriteLine($"{clientSocket.RemoteEndPoint}断开连接");
clientList.Remove(clientSocket);
clientSocket.Close();
}
}
}
客户端
然后改造客户端 客户端不是实时的 而且顺序有问题
先解决 不能实时刷新的问题
还有会有时候接收到连在一起的字符 那是粘包问题 后面改造
主要是我们点击时候发送又输入到屏幕上
其实应该是 点击后 发给服务器等服务器返回才输入屏幕上
/**
*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 msg = new List();
// 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(() =>
{
//发送数据
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
{
//接受数据
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了
区分玩家 解决粘包
这样不知道哪个消息是谁发的
有三种解决方法
1、 加入注册登录功能
2、 在连接服务端成功后先给服务端发送消息设置昵称
3、 在每次发送消息的时候发送昵称
可以设计个数据包
用名字:信息
上面的放名字
这里一改就行
btn.onClick.AddListener(() =>
{
//发送数据
socket.Send(Encoding.UTF8.GetBytes(inputName.text+":"+input.text));
});
然后是粘包
服务器压力大的时候会出现
发送5次然后返回一次 黏在一起
原因 TCP是个"流"协议,所谓流,就是没有界限的一串数据
会有以下4种情况 234都是粘包
先接收到data1,然后接收到data2。
先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部。
先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据。
一次性接收到了data1和data2的全部数据。
相比UDP UDP是个数据包协议 他要么完整要么全丢
服务器客户端都可能发生粘包
1、由Nagle算法造成的发送端的粘包
我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去
2.接收端接收不及TCP缓存区缓存了多个数据时造成的接收端粘包
解决方法 封包拆包
封包
1.数据转为json字符串
2.把json转为byte[]数组A
3.创建一个长度为数组A长度+2字节的字节数组B,依次将2字节的长度和json字节数组A先后输入写入到这个B中
4.将这个字节数组B(数据包)发送给服务端
拆包
1.将最新数据放入DataCache
2、尝试从DataCache中解析数据包,具体代码见上面的Decode
3、一直尝试解析,直到数据不足
一般来说XML json会很大 一般用自定义的二进制格式
数据一般分为这两种 定长的数据和不定长的数据
定长的数据比如:byte,short,int,long,char之类的简单数据,以及仅包含这些类型的类或结构体
不定长的数据比如:字符串string、列表List、字典Dictionary等等,这些都需要进行特殊处理,一般是在数据内容的开头加上一个长度数据。比如写入一个字符串string时,先写入2字节的string的长度,再写入string的具体内容,类似我们上面处理的json字符串。
比如现在我们的数据要改二进制的话
然后谷歌出了个protobuf比较好用 也不用自己写这么多处理