在学习了多线程和套接字之后,跟着老师把聊天室程序完整的做了下来,以下是我的总结:
1. 首先:为大家展示一下这个聊天程序的基本样子,如图:
2. 先来看服务器端,在启动服务按钮下完成设置网络通信的IP和端口号,在这里我们就用本地电脑地址127.0.0.1,以后如果联网,将IP地址改为外网IPV4地址即可,端口号可以任意设置。
public Server() { InitializeComponent(); TextBox.CheckForIllegalCrossThreadCalls = false;//关闭对文本框跨线程检查 } Thread thread = null;//负责监听客户端连接请求的线程 Socket socketWatch = null;//负责服务器的监听功能 private void btnBeginListen_Click(object sender, EventArgs e) { //获得文本框中的IP地址 IPAddress address = IPAddress.Parse(txtIP.Text.Trim()); //创建包含IP和端口号的网络节点对象 IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtProt.Text.Trim())); //创建服务器负责监听的套接字,参数(使用IP4寻址协议,使用流式连接,使用TCP协议传出数据) socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //将负责监听的套接字绑定到唯一的Ip和端口号上 socketWatch.Bind(endpoint); //设置监听队列长度 socketWatch.Listen(10); //创建线程解决Accept()带来的长时间占用当前线程 Thread thread = new Thread(WatchConnection); //设置后台线程 thread.IsBackground = true; //启动线程 thread.Start(); ShowMsg("服务器启动监听成功!"); } void ShowMsg(string msg) { txtMsg.AppendText(msg + "\n"); }
3. 这个时候因为没有任何客户端与之通信,可以使用微软提供的命令提示符的方式对其进行下一步操作:打开cmd,在里面输入IP和端口号,如127.0.0.1空格800,这样就可以测试是否可以连接成功
//保存了服务器端所有负责和客户端通信的套接字 Dictionary<string, Socket> dict = new Dictionary<string, Socket>(); //保存了服务器端所有页面调用通信套接字receive方法的线程 Dictionary<string, Thread> dictThread = new Dictionary<string, Thread>(); // 监听客户端请求的方法 void WatchConnection() { while (true)//持续不断地监听新的客户端的连接请求 { //创建一个新的套接字,开始监听客户端的连接请求,注意:Accept()会阻断当前线程 Socket socketConnection = socketWatch.Accept(); //像列表控件中添加客户端Ip端口字符串,作为客户端唯一标志 lbOnline.Items.Add(socketConnection.RemoteEndPoint.ToString()); //将与客户端通信的套接字对象socketConnection添加到键值集合中,并以客户端IP端口作为键 dict.Add(socketConnection.RemoteEndPoint.ToString(), socketConnection); //创建一个新的线程,即通信线程 ParameterizedThreadStart pts = new ParameterizedThreadStart(ReciveMsg); Thread thr = new Thread(pts); thr.IsBackground = true; thr.SetApartmentState(ApartmentState.STA);//创建并进入一个单线程单元 thr.Start(socketConnection); dictThread.Add(socketConnection.RemoteEndPoint.ToString(), thr); //socketConnection.RemoteEndPoint中保存的是当前连接客户端的IP和端口 ShowMsg("客户端连接成功!" +socketConnection.RemoteEndPoint.ToString()); } }
4. 接下来是服务器向客服端发送数据和接收数据,接收的数据要进行特殊处理,判断数据的是消息还是文件。
//发送消息到客户端 private void btnsend_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(lbOnline.Text)) { MessageBox.Show("请选择要发送的对象!"); } else { string str = txtsend.Text.Trim(); //将 要发送的字符串转成utf8对应的字节数组 byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(str); //获得列表中选中的Key string strClientKey =lbOnline.SelectedItem.ToString(); //通过Key,找到字典集合中对应的某个客户端通信的套接字的send方法,发送给对方 try { dict[strClientKey].Send(arrMsg); ShowMsg("发送了:" + str); } catch (SocketException ex) { ShowMsg("发送数据时异常:" + ex); } catch (Exception ex) { ShowMsg("发送数据时异常:" + ex); } } } //接收客户端消息 void ReciveMsg(object socketClientPara) { Socket socketClient = socketClientPara as Socket; while (true) { //定义一个2M的接收缓存区 byte[] arrMsg = new byte[1024 * 1024 * 2]; //将接收的数据存入arrMsg数组,并返回真正接收到的数据长度 int length = -1; try { length =socketClient.Receive(arrMsg); } catch (SocketException ex) { ShowMsg("异常:" + ex); //从通信套接字删除被中断连接的套接字对象 dict.Remove(socketClient.RemoteEndPoint.ToString()); //从通信线程中删除被中断连接的套接字对象 dictThread.Remove(socketClient.RemoteEndPoint.ToString()); //从列表中移除被中断的Ip和端口号 lbOnline.Items.Remove(socketClient.RemoteEndPoint.ToString()); break; } catch (Exception ex) { ShowMsg("异常:" + ex); break; } if (arrMsg[0] == 0)//判断接收的数据文本第一个字符是0,代表文字;1,代表文件 { //此时是将数组所有元素装成字符串,而真正接收到的只有服务端发过来的字符 string str = System.Text.Encoding.UTF8.GetString(arrMsg, 1,length - 1); ShowMsg("接收到:" + str); } else if (arrMsg[0] == 1) { //在win7中,一个线程调用SaveFileDialog()方法时,要把该线程设置为thr.SetApartmentState(ApartmentState.STA); SaveFileDialog sfd = new SaveFileDialog();//文件保存对象 if (sfd.ShowDialog() ==System.Windows.Forms.DialogResult.OK) {//用户选择好路径后 string fileSave = sfd.FileName;//文件的保存路径 using (FileStream fs = new FileStream(fileSave, FileMode.Create)) {//新建一个文件,保存文件流 fs.Write(arrMsg, 1,length - 1);//去掉标识符 ShowMsg("文件保存成功" + fileSave); } } } } }
5. 使用群发消息时,我们可以这样做
private void btnSendToAll_Click(object sender, EventArgs e) { string str = txtsend.Text.Trim(); byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(str); foreach (Socket s in dict.Values) { s.Send(arrMsg); } ShowMsg("群发完毕!"); }
6. 接下来看客户端如何进行操作的,服务器打开后,客户端需要与服务器的IP和端口一致才可以进行通信,这部分和服务器端代码很类似,首先是判断从文本中后的socket通信地址,然后与服务器连接即可。
public Cilent() { InitializeComponent(); TextBox.CheckForIllegalCrossThreadCalls = false; } Socket socketConnect = null;//负责通信的套接字 private void btnConnect_Click(object sender, EventArgs e) { IPAddress address = IPAddress.Parse(txtIP.Text.Trim()); IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtProt.Text.Trim())); socketConnect = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { socketConnect.Connect(endPoint);//建立与远程主机连接 } catch (Exception ex) { ShowMsg("异常:" + ex); } Thread thread = new Thread(ReciveMsg); thread.IsBackground = true; thread.Start(); }
7. 剩下就是客户端向服务端发送数据和从服务端接收数据。在这里我们将这些数据先转换成二进制流,便于进行TCP数据传输,然后再转换成原来的格式。
//客户端接收数据 void ReciveMsg() { while (true) { //定义一个2M的接收缓存区 byte[] arrMsg = new byte[1024 * 1024 * 2]; //将接收的数据存入arrMsg数组,并返回真正接收到的数据长度 int length = -1; try { length =socketConnect.Receive(arrMsg); } catch (SocketException ex) { ShowMsg("异常:" + ex); break; } catch (Exception ex) { ShowMsg("异常:" + ex); break; } //此时是将数组所有元素装成字符串,而真正接收到的只有服务端发过来的字符 string str = System.Text.Encoding.UTF8.GetString(arrMsg, 0,length); ShowMsg("接收到:" + str); } } void ShowMsg(string msg) { txtMsg.AppendText(msg + "\n"); } //向服务器端发送数据 private void btnsend_Click(object sender, EventArgs e) { string str = txtsend.Text.Trim(); if (!string.IsNullOrEmpty(str)) { //将 要发送的字符串转成utf8对应的字节数组 byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(str); //通过Key,找到字典集合中对应的某个客户端通信的套接字的send方法,发送给对方 byte[] arrMsgSend = new byte[arrMsg.Length + 1]; arrMsgSend[0] = 0; Buffer.BlockCopy(arrMsg, 0, arrMsgSend, 1,arrMsg.Length); socketConnect.Send(arrMsgSend); ShowMsg("发送了:" + str); } }
8. 为了扩展聊天室功能,为它添加了文件传输功能,这里主要是对文件选择和文件处理进行介绍。
//选择发送的文件 private void btnCheckFile_Click(object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog(); if (ofd.ShowDialog() ==System.Windows.Forms.DialogResult.OK) { txtFile.Text = ofd.FileName; } } //向服务器发送文件 private void btnSendFile_Click(object sender, EventArgs e) { //用文件流打开用户选择的文件 using (FileStream fs = new FileStream(txtFile.Text, FileMode.Open)) { byte[] arrMsg = new byte[1024 * 1024 * 2]; //将文件数据存入arrMsg数组,并返回真正接收到的数据长度 int length = fs.Read(arrMsg, 0,arrMsg.Length); //新建一个比原来数组长度加1的数组 byte[] arrFileSend = new byte[length + 1]; //将数组的第一个值设为标识符,1代表是文件类型 arrFileSend[0] = 1; //数组复制1 //for (int i = 0; i < length; i++) //{ // arrFileSend[i + 1] = arrMsg[i]; //} //数组复制2 //arrMsg.CopyTo(arrFileSend, length); //数组复制3,将arrMsg数组里的元素,从第0个开始拷贝,拷贝到arrFileSend数组里,从第1个位置开始存放,一共拷贝length个长度 Buffer.BlockCopy(arrMsg, 0, arrFileSend, 1,length); //发送包含了标识符的新数组到服务器 socketConnect.Send(arrFileSend); } }
9. 经过这个小项目,使我在委托,线程,套接字,文件处理等方面都有了新的认识,也对现有的知识进行了巩固,感谢传智老师认真详细的见解。