一个基于TCP/IP的小项目,实现广播消息的功能。(超详细版)
1.结合现状 功能分析
该功能基于上个项目的改进,主要是通过对服务器端代码的修改,以及对客户端作少许修改,实现开启多客户端时,一个客户端发送消息,达到对所有客户端广播的效果。可参考网吧里的点歌系统,比如某某用户在网吧点了一首歌,其他用户电脑的左下角都会弹出一个某某用户点了一首七里香,或者游戏里面的频道聊天,每个人发完消息后,聊天室里的人都知道你发的消息了,就像下图一样,这也正是做这个功能的初衷吧。
2.图说代码
代码细说:
服务器里面定义了两个字段,一个用于服务器与客户端的连接,另一个目的在于做一个接受广播的客户端表单。
服务器里有四个函数,分别对应着客户端监听、客户端连接、接受客户端消息和发报广播。
客户端定义了一个字段
客户端包含4个函数,分别为建立连接,接受广播,非后台的发送消息线程、发送消息四部分
操作流程:
1)开启服务器,即黑线①的过程,启动监听。
2)开启客户端,自动根据IP连接服务器,即绿线②。
3)客户端1发送消息至服务器,服务器广播消息至客户端2,即红线③。
3.代码实现
服务器端包含一个主函数和一个ServerControl类
主函数:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ServerTest { class Program { static void Main(string[] args) { // 调用构造函数,使用Start方法 ServerControl server = new ServerControl(); server.Start(); Console.ReadKey(); } } }
ServerControl类:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ServerTest { public class ServerControl { // 声明变量(使用Socket需using System.Net.Sockets;) private Socket serverSocket; // 声明一个集合 private List<Socket> clientList; // 自定义有参构造函数,包含两个方法(IP地址,流程传输方式,TCP协议) public ServerControl() { serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); clientList = new List<Socket>(); } // 创建启动方法(IPEndPoint用于指定地址及端口初始化,需using System.Net;) public void Start() { // 服务器启动 // 绑定IP地址(为任意IP)与端口(设置为12345) serverSocket.Bind(new IPEndPoint(IPAddress.Any,12345)); serverSocket.Listen(10); Console.WriteLine("服务器启动成功"); // 开启线程:目的实现服务器和客户端一对多连接 Thread threadAccept = new Thread(Accept); threadAccept.IsBackground = true; threadAccept.Start(); } // Accept方法测试:接收客户端连接 private void Accept() { // 接收客户端方法,会挂起当前线程(.RemoteEndPoint表示远程地址) Socket client = serverSocket.Accept(); IPEndPoint point = client.RemoteEndPoint as IPEndPoint; Console.WriteLine(point.Address + "[" + point.Port + "] 连接成功!"); clientList.Add(client); // 开启一个新线程线程,实现消息多次接收 Thread threadReceive = new Thread(Receive); threadReceive.IsBackground = true; threadReceive.Start(client); // 尾递归 Accept(); } // Receive方法的使用测试 // 接收客户端发送过来的消息,以字节为单位进行操作 // 该方法会阻塞当前线程,所以适合开启新的线程使用该方法 // Accept()中将Receive作为线程传递对象,所以要注意一点,使用线程传递对象只能是object类型的!! private void Receive(object obj) { // 将object类型强行转换成socket Socket client = obj as Socket; IPEndPoint point = client.RemoteEndPoint as IPEndPoint; // 此处的异常抛出主要针对客户端异常的问题 // 比如,客户端关闭或者连接中断 // 程序会停留在int msgLen = client.Receive(msg);这段代码,而导致无法继续往下走 try { byte[] msg = new byte[1024]; // 实际接收到字节数组长度,该方法会阻塞当前线程,即(client.Receive(msg)开始挂起) // 同时,这里还是尾递归挂起处 int msgLen = client.Receive(msg); // 将msg装换成字符串 string msgStr = point.Address + "[" + point.Port + "]:" + Encoding.UTF8.GetString(msg, 0, msgLen); Console.WriteLine(msgStr); // 调用广播函数 Broadcast(client,msgStr); // 尾递归实现多条消息的接收;和while同理。 Receive(client); } catch { Console.WriteLine(point.Address + "[" + point.Port + "]积极断开"); // 若客户端中断,则将他在集合中删除 clientList.Remove(client); } } private void Broadcast(Socket clientOther,string msg) { foreach(var client in clientList) { if(client == clientOther) { // 不做任何响应 } else { client.Send(Encoding.UTF8.GetBytes(msg)); } } } } }
客户端包含一个主函数和一个ClientControl类
主函数:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ClientTest { class Program { static void Main(string[] args) { // 调用构造函数 ClientControl client = new ClientControl(); // 输入本机IP与端口号 client.Connect("129.211.7.135", 12345); // 启动send方法 client.Send(); Console.ReadKey(); } } }
ClientControl类:
using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ClientTest { public class ClientControl { // 声明变量 private Socket clientSocket; // 自定义有参构造方法((IP地址,流程传输方式,TCP协议)) public ClientControl() { clientSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp); } // 创建通过IP与端口号连接的方法 public void Connect(string ip,int port) { clientSocket.Connect(ip, port); Console.WriteLine("连接服务器成功"); // 客户端接收服务器消息的线程 Thread threadReceive = new Thread(Receive); threadReceive.IsBackground = true; threadReceive.Start(); } // 用于测试服务器向客户端返回一条消息 private void Receive() { while(true) { try { // 用于接收服务器的回复信息 byte[] msg = new byte[1024]; int msgLen = clientSocket.Receive(msg); Console.WriteLine("服务器:"+Encoding.UTF8.GetString(msg,0,msgLen)); } // 异常处理方法 catch { Console.WriteLine("服务器积极拒绝!!"); // 退出while循环 break; } } } // Send方法测试:即发送消息,以字节为单位 public void Send() { Thread threadSend = new Thread(ReadAndSend); // 将该线程设为非后台线程。 // threadSend.IsBackground = true; threadSend.Start(); } private void ReadAndSend() { // 提示操作方法 Console.WriteLine("请输入发送至服务器的内容或者输入quit退出"); // 输入内容 string msg = Console.ReadLine(); // 非退出情况下操作方式,使用while可以持续不断的接收用户输入 while (msg != "quit") { clientSocket.Send(Encoding.UTF8.GetBytes(msg)); msg = Console.ReadLine(); } } } }
4.实现过程
该过程将服务器部署至腾讯云服务器,分别在腾讯云服务器和本地PC上各开启2个客户端演示广播过程。
若没有多余的电脑或者云服务器,可将客户端主函数里面的IP地址代码改为127.0.0.1,即可完成本地测试。