聊天室:服务器端-------------客户端
最终演示展示图:
一. 服务器端
对服务端为了让主窗体后台不处理具体业务逻辑,因此对服务端进行了封装,专门用来处理某个客户端通信的过程。
而由于通信管理类中需要处理具体与某个客户端的通信业务,所以在构造函数中传入了具体的套接字对象。
针对消息提醒:由于需要再通信管理类中进行消息提示,而需要调用主窗体的ShowMsg方法。因此将打印消息的方法通过委托传给了通信管理类的构造函数
同理针对意外关闭的客户端连接也同样通过委托将移除客户端的方法传给了通信管理类。
因此,再通信管理类的全局变量里就有了如下的定义:
//与某个客户端通信套接字 Socket sokMsg = null; //通信线程 Thread thrMsg = null; //创建一个委托对象, 在窗体显示消息的方法 DGShowMsg dgShow = null; //创建一个关闭连接的方法 DGCloseConn dgCloseConn = null;//通信管理类的构造函数public MsgConnection(Socket sokMsg, DGShowMsg dgShow, DGCloseConn dgCloseConn).........
1.0 开始监听
1.1.1 创建监听套接字
//创建监听套接字,使用ip4协议,流式传输,tcp链接 sokWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
1.1.2 绑定端口
//获取网络节点对象 IPAddress address = IPAddress.Parse(txbIp.Text); IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txbPort.Text)); // 绑定端口(其实内部 就向系统的端口表中注册了一个端口,并指定了当前程序句柄) sokWatch.Bind(endPoint);
1.1.3 设置监听队列,指限制同时处理的连接请求数,即同时处理的客户端连接请求。
sokWatch.Listen(10);
1.1.4 开始监听,调用监听线程 执行 监听套接字的监听方法。
thrWatch = new Thread(WatchConncting); thrWatch.IsBackground = true; thrWatch.Start(); ShowMsg("服务器启动!");
2.0 服务器监听方法 + void WatchConncting()
void WatchConncting() { try { //循环监听客户端的连接请求。 while (isWatch) { //2.4开始监听,返回了一个通信套接字 Socket sockMsg = sokWatch.Accept(); //2.5 创建通信管理类 MsgConnection conn = new MsgConnection(sockMsg, ShowMsg, RemoveClient); //将当前连接成功的【与客户端通信的套接字】的标识保存起来,并显示到列表中 //将远程客户端的 ip 和 端口 字符串 存入列表 listOnline.Items.Add(sockMsg.RemoteEndPoint.ToString()); //将服务器端的通信套接字存入字典集合。 dictConn.Add(sockMsg.RemoteEndPoint.ToString(), conn); ShowMsg("有客户端连接了!"); } } catch (Exception ex) { ShowMsg("异常" + ex); } }
3.0 服务端向指定的客户端发送消息
string strClient = listOnline.Text; if (dictConn.ContainsKey(strClient)) { string strMsg = txtInput.Text.Trim(); ShowMsg("向客户端【" + strClient + "】说:" + strMsg); //通过指定的套接字将字符串发送到指定的客户端 try { dictConn[strClient].Send(strMsg); } catch (Exception ex) { ShowMsg("异常" + ex.Message); } }
4.0 根据要中断的客户端ipport关闭连接 + void RemoveClient(string clientIpPort)
//1.0 移除列表中的项 listOnline.Items.Remove(clientIpPort); //2.0 关闭通信管理类 dictConn[clientIpPort].Close(); //3.0 从字典中移除对应的通信管理类的项 dictConn.Remove(clientIpPort);
5.0 选择要发送的文件
OpenFileDialog ofd = new OpenFileDialog(); if (ofd.ShowDialog() == System.Windows.Forms.DialogResult.OK) { //将选中的要发送的文件路径,显示到文本框中。 txtFilePath.Text = ofd.FileName; }
6.0 发送文件
string strClient = listOnline.Text; if (dictConn.ContainsKey(strClient)) { string strMsg = txtInput.Text.Trim(); //通过指定的套接字将字符串发送到指定的客户端 try { dictConn[strClient].SendFile(txtFilePath.Text.Trim()); } catch (Exception ex) { ShowMsg("异常" + ex.Message); } }
7.0 向指定的客户端发送抖屏
string strClient = listOnline.Text; if (dictConn.ContainsKey(strClient)) { string strMsg = txtInput.Text.Trim(); //通过指定的套接字将字符串发送到指定的客户端 try { dictConn[strClient].SendShake(); } catch (Exception ex) { ShowMsg("异常" + ex.Message); } }
8.0 打印消息 + ShowMsg(string strmsg)
this.txtShow.AppendText(strmsg + "\r\n");
服务器端通信管理类,负责处理与某个客户端通信的过程
public class MsgConnection { //与某个客户端通信套接字 Socket sokMsg = null; //通信线程 Thread thrMsg = null; //创建一个委托对象, 在窗体显示消息的方法 DGShowMsg dgShow = null; //创建一个关闭连接的方法 DGCloseConn dgCloseConn = null; #region 1.0 构造函数 public MsgConnection(Socket sokMsg, DGShowMsg dgShow, DGCloseConn dgCloseConn) { this.sokMsg = sokMsg; this.dgShow = dgShow; this.dgCloseConn = dgCloseConn; //创建通信线程,负责调用通信套接字,来接收客户端消息。 thrMsg = new Thread(ReceiveMsg); thrMsg.IsBackground = true; thrMsg.Start(this.sokMsg); } #endregion bool isReceive = true; #region 2.0 接收客户端发送的消息 void ReceiveMsg(object obj) { Socket sockMsg = obj as Socket; //3 通信套接字 监听客户端的消息,传输的是byte格式。 //3.1 开辟了一个 1M 的空间,创建的消息缓存区,接收客户端的消息。 byte[] arrMsg = new byte[1024 * 1024 * 1]; try { while (isReceive) { //注意:Receive也会阻断当前的线程。 //3.2 接收客户端的消息,并存入消息缓存区。 //并 返回 真实接收到的客户端数据的字节长度。 int realLength = sockMsg.Receive(arrMsg); //3.3 将接收的消息转成字符串 string strMsg = System.Text.Encoding.UTF8.GetString(arrMsg, 0, realLength); //3.4 将消息显示到文本框 dgShow(strMsg); } } catch (Exception ex) { //调用窗体类的关闭移除方法 dgCloseConn(sokMsg.RemoteEndPoint.ToString()); //显示消息 dgShow("客户端断开连接!"); } } #endregion #region 3.0 向客户端发送文本消息 + void Send(string msg) /// <summary> /// 3.0 向客户端发送文本消息 /// </summary> /// <param name="msg"></param> public void Send(string msg) { byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(msg); //通过指定的套接字将字符串发送到指定的客户端 try { sokMsg.Send(MakeNewByte("str",arrMsg)); } catch (Exception ex) { dgShow("异常" + ex.Message); } } #endregion #region 4.0 向客户端发送文件 + void SendFile(string strPath) /// <summary> /// 4.0 向客户端发送文件 /// </summary> /// <param name="strFilePath"></param> public void SendFile(string strFilePath) { //4.1 读取要发送的文件 byte[] arrFile = System.IO.File.ReadAllBytes(strFilePath); //4.2 向客户端发送文件 sokMsg.Send(MakeNewByte("file", arrFile)); } #endregion #region 4.1 向客户端发送抖屏命令 + void SendShake() /// <summary> /// 4.1 向客户端发送抖屏命令 /// </summary> public void SendShake() { sokMsg.Send(new byte[1] { 2 }); } #endregion #region 5.0 返回带标识的新数组 + byte[] MakeNew(string type, byte[] oldArr) /// <summary> /// 返回带标识的新数组 /// </summary> /// <param name="type"></param> /// <param name="oldArr"></param> /// <returns></returns> public byte[] MakeNewByte(string type, byte[] oldArr) { //5.1 创建一个新数组(是原数组长度 +1) byte[] newArrFile = new byte[oldArr.Length + 1]; //5.2 将原数组数据复制到新数组中(从新数组下标为1的位置开始) oldArr.CopyTo(newArrFile, 1); //5.3 根据内容类型为新数组第一个元素设置标识符号 switch (type.ToLower()) { case "str": newArrFile[0] = 0; //只能存0-255之间的数值 break; case "file": newArrFile[0] = 1; break; default: newArrFile[0] = 2; break; } return newArrFile; } #endregion #region 6.0 关闭通信 /// <summary> /// 关闭通信 /// </summary> public void Close() { isReceive = false; sokMsg.Close(); sokMsg = null; } #endregion }
二 . 客户端
1.0 发送连接服务端请求
try { //1.0创建连接套接字,使用ip4协议,流式传输,tcp链接 sokMsg = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2.0获取要链接的服务端节点 //2.1获取网络节点对象 IPAddress address = IPAddress.Parse(txtIp.Text); IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text)); //3 向服务端发送链接请求。 sokMsg.Connect(endPoint); ShowMsg("连接服务器成功!"); //4 开启通信线程 thrMsg = new Thread(ReceiveMsg); thrMsg.IsBackground = true; //win7, win8 需要设置客户端通信线程同步设置,才能在接收文件时打开文件选择框 thrMsg.SetApartmentState(ApartmentState.STA); thrMsg.Start(); } catch (Exception ex) { ShowMsg("连接服务器失败!" + ex.Message); }
2.0 接收服务端消息
//准备一个消息缓冲区域 byte[] arrMsg = new byte[1024 * 1024 * 1]; try { while (isReceive) { //接收 服务器发来的数据,因为包含了一个标示符,所以内容的真实长度应该-1 int realLength = sokMsg.Receive(arrMsg)-1; switch (arrMsg[0]) { //文本 case 0: GetMsg(arrMsg,realLength); break; //文件 case 1: GetFile(arrMsg,realLength); break; default: ShakeWindow(); break; } } } catch (Exception ex) { sokMsg.Close(); sokMsg = null; ShowMsg("服务器断开连接!"); }
2.1 接收服务端文本消息 + GetMsg(byte[] arrContent, int realLength)
//获取接收的内容,去掉第一个标示符。 string strMsg = System.Text.Encoding.UTF8.GetString(arrContent, 1, realLength); ShowMsg("服务器说:" + strMsg);
2.2 保存文件 + GetFile(byte[] arrContent, int realLength)
SaveFileDialog sfd = new SaveFileDialog(); if (sfd.ShowDialog() == System.Windows.Forms.DialogResult.OK) { string savaPath = sfd.FileName; //使用文件流,保存文件 using (System.IO.FileStream fs = new System.IO.FileStream(savaPath, System.IO.FileMode.OpenOrCreate)) { //将收到的文件数据数组,写入硬盘。 fs.Write(arrContent, 1, realLength); } ShowMsg("保存文件到 【" + savaPath + "】成功!"); }
2.3 抖屏 + void ShakeWindow()
// 保存当前窗体位置 Point oldPoint = this.Location; for (int i = 0; i < 15; i++) { //随机生成新位置 Point newPoint = new Point(oldPoint.X + ran.Next(8), oldPoint.Y + ran.Next(8)); //将新位置设置给窗体 this.Location = newPoint; System.Threading.Thread.Sleep(25); this.Location = oldPoint; //休息15毫秒 System.Threading.Thread.Sleep(25); }
3.0 客户端发送消息到服务端
string strMsg = txtInput.Text.Trim(); if (strMsg != "") { byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(strMsg); try { sokMsg.Send(arrMsg); } catch (Exception ex) { ShowMsg("发送消息失败!" + ex.Message); } } else { MessageBox.Show("未输入任何信息!"); }
4.0 展示消息方法 + ShowMsg(string strmsg)
this.txtShow.AppendText(strmsg + "\r\n");
注:小弟不断学习中,还希望园友指导交流。同时也作为个人学习的一个小记录。【好记性不如烂笔头】(*^__^*)
源码分享: http://download.csdn.net/detail/huayuqiang/8270749