UN2L3——Socket的TCP同步通信
服务端Socket的TCP同步通信
服务端Socket的基本思路如下:
创建套接字Socket
用Bind
方法将套接字与本地地址绑定
用Listen
方法监听
用Accept
方法等待客户端连接
建立连接,Accept
返回新套接字
对返回的新套接字用Send
和Receive
相关方法收发数据
用Shutdown
方法释放连接
关闭套接字
根据以上思路,实现最基础的服务端代码
两台电脑在同一局域网下连接的实践
如果可以使用两台电脑并连接同一路由器,那么不妨将服务端代码中的IP地址改为在设备在路由器下的IP地址(在cmd使用ipconfig
可查询)
并且客户端使用服务器的IP地址,如果可行,就可以实现客户端和服务端相互连接,使用两台设备学习的体会效果更好
游戏服务器一般使用8080
作为自己的端口号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 static void Main (string [] args ){ Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1" ), 8080 ); socketTcp.Bind(ipPoint); } catch (Exception e) { Console.WriteLine("绑定报错" + e.Message); return ; } socketTcp.Listen(1024 ); Console.WriteLine("服务端绑定监听结束,等待客户端连入" ); Socket socketClient = socketTcp.Accept(); Console.WriteLine("有客户端连入了" ); socketClient.Send(Encoding.UTF8.GetBytes("欢迎连入服务端" )); byte [] result = new byte [1024 ]; int receiveNum = socketClient.Receive(result); Console.WriteLine("接受到了{0}发来的消息:{1}" , socketClient.RemoteEndPoint.ToString(), Encoding.UTF8.GetString(result, 0 , receiveNum)); socketClient.Shutdown(SocketShutdown.Both); socketClient.Close(); Console.WriteLine("按任意键退出" ); Console.ReadKey(); }
让服务端可以连接多个客户端
以上的代码一次只能连接一个客户端,随后服务端的运行就会终止
我们可以通过开启一个新线程并使用循环来反复监听客户端的连入,同时做到不卡主线程,
监听到客户端连入就将返回的新Socket
管理起来,分别管理收发消息
同时再开启一个用于接收消息的线程,循环检测是否有新的数据,有就接收数据,做到不卡主线程
这样就可以让服务端连接多个客户端
isClose
:用于控制各个线程的循环终止,当在终端输入Quit
指令时就为true
Main
:用于初始化服务端Socket,并开启监听连入线程和接收消息线程,然后检测终端输入,输入Quit
就关闭服务器,B:
就广播消息
AcceptClientConnent
:单独线程,循环监听客户端的连入,监听到客户端连入就将新Socket添加到列表内管理
ReceiveMsg
:单独线程,循环所有列表内的客户端Socket,检测是否有要接收的数据,当检测到有数据就执行receive
方法
HandleMsg
:采用线程池,将接收到的数据进行处理,输出到终端上,数据的接收和处理一般是分开线程,避免处理信息卡住接收消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 using System.Net;using System.Net.Sockets;using System.Text;namespace TeachTcpServerExecises { internal class Program { static Socket? socket; static List<Socket> clientSockets = new List<Socket>(); static bool isClose = false ; static void Main (string [] args ) { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("192.168.1.111" ), 8080 ); socket.Bind(ipPoint); socket.Listen(1024 ); Thread accpetThread = new Thread(AcceptClientConnent); accpetThread.Start(); Thread receiveThread = new Thread(ReceiveMsg); receiveThread.Start(); while (true ) { string ? input = Console.ReadLine(); if (input == null ) continue ; if (input == "Quit" ) { isClose = true ; for (int i = 0 ; i < clientSockets.Count; i++) { clientSockets[i].Shutdown(SocketShutdown.Both); clientSockets[i].Close(); } clientSockets.Clear(); break ; } else if (input.Substring(0 , 2 ) == "B:" ) { for (int i = 0 ;i < clientSockets.Count; i++) { clientSockets[i].Send(Encoding.UTF8.GetBytes(input.Substring(2 ))); } } } } static void AcceptClientConnent () { while (!isClose) { if (socket == null ) continue ; Socket clientSocket = socket.Accept(); clientSockets.Add(clientSocket); clientSocket.Send(Encoding.UTF8.GetBytes("欢迎你连入服务端" )); } } static void ReceiveMsg () { Socket clientSocket; byte [] result = new byte [1024 * 1024 ]; int receiveNum; int i = 0 ; while (!isClose) { for (i = 0 ; i < clientSockets.Count; i++) { clientSocket = clientSockets[i]; if (clientSocket.Available > 0 ) { receiveNum = clientSocket.Receive(result); ThreadPool.QueueUserWorkItem(HandleMsg, (clientSocket, Encoding.UTF8.GetString(result, 0 , receiveNum))); } } } } static void HandleMsg (object ? obj ) { if (obj == null ) return ; (Socket s, string str) info = ((Socket s, string str))obj; Console.WriteLine($"收到了客户端{info.s.RemoteEndPoint} 发来的消息:{info.str} " ); } } }
使用面向对象封装Socket
我们可以通过面向对象来封装服务器Socket和连接客户端的Socket,使连接客户端Socket和服务端Socket的逻辑各自的逻辑分离开
Program.cs
Main
:创建并初始化服务器SocketServerSocket
,然后循环监听终端输入,输入Quit
就关闭服务端,B:
就调用服务端广播消息方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 internal class Program { static void Main (string [] args ) { ServerSocket socket = new ServerSocket(); socket.Start("192.168.1.111" , 8080 , 1024 ); Console.WriteLine("服务器开启成功" ); while (true ) { string ? input = Console.ReadLine(); if (input == null ) continue ; if (input == "Quit" ) { socket.Close(); } else if (input.Substring(0 , 2 ) == "B:" ) { socket.BroadCast(input.Substring(2 )); } } } }
ServerSocket.cs
ServerSocket
将服务端Socket常用的方法封装起来,同时管理所有的连接客户端的SocketClientSocket
clientDic
:用于管理各个连接客户端的Socket,可以通过ID来获取单个Socket
isClose
:用于控制各个线程的循环终止,在Close
方法中设置为true
Start
:提供给外部开启并初始化服务端Socket的方法,同时通过线程池开启监听连入和消息接收的线程
Close
:关闭并释放所有的Socket,关闭监听连入和消息接收的线程
Accept
:循环监听客户端的连入,监听到连入就实例化ClientSocket
并加入字典管理
Receive
:循环检测字典内的ClientSocket
是否有消息需要接收,检测到就执行ClientSocket
的接收消息方法
BroadCast
:让所有的ClientSocket
发送传入的广播消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 using System.Net;using System.Net.Sockets;namespace TeachTcpServerExecises2 { internal class ServerSocket { public Socket? socket; public Dictionary<int , ClientSocket> clientDic = new Dictionary<int , ClientSocket>(); private bool isClose; public void Start (string ip, int port, int num ) { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port); socket.Bind(ipPoint); socket.Listen(num); ThreadPool.QueueUserWorkItem(Accept); ThreadPool.QueueUserWorkItem(Receive); isClose = false ; } public void Close () { if (socket == null ) return ; foreach (var client in clientDic.Values) { client.Close(); } clientDic.Clear(); socket.Shutdown(SocketShutdown.Both); socket.Close(); socket = null ; isClose = true ; } private void Accept (object ? obj ) { if (socket == null ) return ; while (!isClose) { try { Socket clientSocket = socket.Accept(); ClientSocket client = new ClientSocket(clientSocket); client.Send("欢迎连入服务器" ); lock (clientDic) { clientDic.Add(client.clientID, client); } } catch (SocketException e) { Console.WriteLine($"客户端连入报错:{e.Message} " ); } } } private void Receive (object ? obj ) { while (!isClose) { lock (clientDic) { if (clientDic.Count > 0 ) { foreach (ClientSocket client in clientDic.Values) { client.Receive(); } } } } } public void BroadCast (string info ) { lock (clientDic) { foreach (var client in clientDic.Values) { client.Send(info); } } } } }
ClientSocket.cs
ClientSocket
将连接客户端的Socket常用的方法封装起来,并由ServerSocket
管理
CLIENT_BEGIN_ID
:连接客户端的Socket起始ID,每多一个ClientSocket
就累加1
clientID
:ClientSocket
的ID,便于从字典内获取
Connented
:是否连接
Close
:关闭并释放Socket
Send
:发送消息
Receive
:接收消息,当接收到消息就将从线程池开启一个新MsgHandle
线程去处理消息
MsgHandle
:处理消息方法,将接收到的消息输出到终端上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 using System.Net.Sockets;using System.Text;namespace TeachTcpServerExecises2 { internal class ClientSocket { private static int CLIENT_BEGIN_ID = 1 ; public int clientID; private Socket socket; public bool Connented => socket.Connected; public ClientSocket (Socket socket ) { this .clientID = CLIENT_BEGIN_ID; this .socket = socket; CLIENT_BEGIN_ID++; } public void Close () { if (this .socket != null ) { socket.Shutdown(SocketShutdown.Both); socket.Close(); } } public void Send (string message ) { if (socket != null ) { try { socket.Send(Encoding.UTF8.GetBytes(message)); } catch (Exception e) { Console.WriteLine($"向客户端{socket.RemoteEndPoint} 发送消息失败:{e.Message} " ); Close(); } } } public void Receive () { if (socket == null ) return ; try { if (socket.Available > 0 ) { byte [] result = new byte [1024 * 5 ]; int receiveNum = socket.Receive(result); ThreadPool.QueueUserWorkItem(MsgHandle, Encoding.UTF8.GetString(result, 0 , receiveNum)); } } catch (Exception e) { Console.WriteLine($"从客户端{socket.RemoteEndPoint} 接收消息失败:{e.Message} " ); Close(); } } private void MsgHandle (object ? obj ) { if (obj == null ) return ; string ? str = obj as string ; Console.WriteLine($"收到客户端{socket.RemoteEndPoint} 发来的消息:{str} " ); } } }
客户端Socket的TCP同步通信
客户端Socket的基本思路如下:
创建套接字Socket
用Connect
方法与服务端相连
用Send
和Receive
相关方法收发数据
用Shutdown
方法释放连接
关闭套接字
根据以上思路,实现最基础的客户端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void Start (){ Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1" ), 8080 ); try { socket.Connect(ipPoint); } catch (SocketException e) { if (e.ErrorCode == 10061 ) print("服务器拒绝连接" ); else print("连接服务器失败" + e.ErrorCode); return ; } byte [] receiveBytes = new byte [1024 ]; int receiveNum = socket.Receive(receiveBytes); print("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0 , receiveNum)); socket.Send(Encoding.UTF8.GetBytes("你好,我是唐老狮的客户端" )); socket.Shutdown(SocketShutdown.Both); socket.Close(); }
客户端可随时收发服务端消息且不阻塞主线程
以上的代码在连接服务端时会阻塞主线程,也不能随时和服务端通信
我们可以将网络连接相关的内容封装到一个单例模式管理器内,并开放接口给外部,每帧检测是否接收到消息
sendMessageQueue
:用于发送消息的队列,公共容器,主线程往sendMessageQueue
放,发送线程从sendMessageQueue
取
receiveMessageQueue
:用于接收消息的队列,公共容器,接收线程往receiveMessageQueue
放,主线程从receiveMessageQueue
取
Connent
:传入服务器的IP地址与端口号,连接对应的服务器,同时通过线程池开启发送消息和接收消息的线程
Update
:每帧都检测receiveMessageQueue
内是否存在消息,若存在,就将消息输出到控制台和屏幕上
Close
:关闭并释放客户端Socket,在管理器被销毁时会调用
Send
:将外部要发送的消息传入到sendMessageQueue
内,等待发送消息线程将消息转换为字节数组发送到服务端
SendMessage
:发送消息的线程,循环检测sendMessageQueue
内是否由要发送的消息,有就转换为字节数组发送到服务端
ReceiveMessage
:接收消息的线程,循环检测是否有消息需要接收,有就将消息转换为字符串传入到receiveMessageQueue
内
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 using System.Collections.Generic;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading;using UnityEngine;using UnityEngine.UI;public class NetManager : MonoBehaviour { private static NetManager instance; public static NetManager Instance => instance; private void Awake () { instance = this ; DontDestroyOnLoad(gameObject); } public Text printMessage; private Socket socket = null ; private Queue<string > sendMessageQueue = new Queue<string >(); private Queue<string > receiveMessageQueue = new Queue<string >(); private byte [] receiveBytes = new byte [1024 * 1024 ]; private int receiveNum = 0 ; private bool isConnented; void Update () { if (receiveMessageQueue.Count > 0 ) { printMessage.text = receiveMessageQueue.Dequeue(); } } private void OnDestroy () { Close(); } public void Connent (string ip, int port ) { if (isConnented) return ; socket ??= new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port); try { socket.Connect(ipPoint); isConnented = true ; ThreadPool.QueueUserWorkItem(SendMessage); ThreadPool.QueueUserWorkItem(ReceiveMessage); Send($"来自唐老狮的客户端" ); } catch (SocketException e) { if (e.ErrorCode == 10061 ) { printMessage.text = "服务器拒绝连接" ; print("服务器拒绝连接" ); } else { printMessage.text = $"连接失败:{e.ErrorCode} , {e.Message} " ; print($"连接失败:{e.ErrorCode} , {e.Message} " ); } } } public void Close () { if (socket != null ) { if (socket.Connected) socket.Shutdown(SocketShutdown.Both); socket.Close(); isConnented = false ; } } public void Send (string info ) { sendMessageQueue.Enqueue(info); } private void SendMessage (object obj ) { while (isConnented) { if (sendMessageQueue.Count > 0 ) { socket.Send(Encoding.UTF8.GetBytes(sendMessageQueue.Dequeue())); } } } private void ReceiveMessage (object obj ) { while (isConnented) { if (socket.Available > 0 ) { receiveNum = socket.Receive(receiveBytes); receiveMessageQueue.Enqueue(Encoding.UTF8.GetString(receiveBytes, 0 , receiveNum)); } } } }