C# Winform 的简易聊天程序
[第一篇 - 两端通信]
程序简介
本聊天程序支持局域网内部客户端与服务端之间的互相通信.
原理
启动服务端后,服务端通过持续监听客户端发来的请求,一旦监听到客户端传来的信息后,两端便可以互发信息了.服务端需要绑定一个IP,用于客户端在网络中寻找并建立连接.信息发送原理:将手动输入字符串信息转换成机器可以识别的字节数组,然后调用套接字的Send()方法将字节数组发送出去.信息接收原理:调用套接字的Receive()方法,获取对端传来的字节数组,然后将其转换成人可以读懂的字符串信息.
界面设计 - 服务端
IP文本框 name: txtIP port(端口号)文本框 name: txtPORT 聊天内容文本框 name: txtMsg 发送信息文本框 name:txtSendMsg
启动服务按钮 name: btnServerConn 发送信息按钮name: btnSendMsg
服务端代码:
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Drawing;
- using System.Linq;
- using System.Text;
- using System.Windows.Forms;
- using System.Threading;
- using System.Net.Sockets;
- using System.Net;
- namespace ChatServer
- {
- public partial class FServer : Form
- {
- public FServer()
- {
- InitializeComponent();
- //关闭对文本框的非法线程操作检查
- TextBox.CheckForIllegalCrossThreadCalls = false;
- }
- Thread threadWatch = null; //负责监听客户端的线程
- Socket socketWatch = null; //负责监听客户端的套接字
- private void btnServerConn_Click(object sender, EventArgs e)
- {
- //定义一个套接字用于监听客户端发来的信息 包含3个参数(IP4寻址协议,流式连接,TCP协议)
- socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
- //服务端发送信息 需要1个IP地址和端口号
- IPAddress ipaddress = IPAddress.Parse(txtIP.Text.Trim()); //获取文本框输入的IP地址
- //将IP地址和端口号绑定到网络节点endpoint上
- IPEndPoint endpoint = new IPEndPoint(ipaddress, int.Parse(txtPORT.Text.Trim())); //获取文本框上输入的端口号
- //监听绑定的网络节点
- socketWatch.Bind(endpoint);
- //将套接字的监听队列长度限制为20
- socketWatch.Listen(20);
- //创建一个监听线程
- threadWatch = new Thread(WatchConnecting);
- //将窗体线程设置为与后台同步
- threadWatch.IsBackground = true;
- //启动线程
- threadWatch.Start();
- //启动线程后 txtMsg文本框显示相应提示
- txtMsg.AppendText("开始监听客户端传来的信息!" + "\r\n");
- }
- //创建一个负责和客户端通信的套接字
- Socket socConnection = null;
- ///
- /// 监听客户端发来的请求
- ///
- private void WatchConnecting()
- {
- while (true) //持续不断监听客户端发来的请求
- {
- socConnection = socketWatch.Accept();
- txtMsg.AppendText("客户端连接成功" + "\r\n");
- //创建一个通信线程
- ParameterizedThreadStart pts = new ParameterizedThreadStart(ServerRecMsg);
- Thread thr = new Thread(pts);
- thr.IsBackground = true;
- //启动线程
- thr.Start(socConnection);
- }
- }
- ///
- /// 发送信息到客户端的方法
- ///
- ///发送的字符串信息
- private void ServerSendMsg(string sendMsg)
- {
- //将输入的字符串转换成 机器可以识别的字节数组
- byte[] arrSendMsg = Encoding.UTF8.GetBytes(sendMsg);
- //向客户端发送字节数组信息
- socConnection.Send(arrSendMsg);
- //将发送的字符串信息附加到文本框txtMsg上
- txtMsg.AppendText("So-flash:" + GetCurrentTime() + "\r\n" + sendMsg + "\r\n");
- }
- ///
- /// 接收客户端发来的信息
- ///
- ///客户端套接字对象
- private void ServerRecMsg(object socketClientPara)
- {
- Socket socketServer = socketClientPara as Socket;
- while (true)
- {
- //创建一个内存缓冲区 其大小为1024*1024字节 即1M
- byte[] arrServerRecMsg = new byte[1024 * 1024];
- //将接收到的信息存入到内存缓冲区,并返回其字节数组的长度
- int length = socketServer.Receive(arrServerRecMsg);
- //将机器接受到的字节数组转换为人可以读懂的字符串
- string strSRecMsg = Encoding.UTF8.GetString(arrServerRecMsg, 0, length);
- //将发送的字符串信息附加到文本框txtMsg上
- txtMsg.AppendText("天之涯:" + GetCurrentTime() + "\r\n" + strSRecMsg + "\r\n");
- }
- }
- //发送信息到客户端
- private void btnSendMsg_Click(object sender, EventArgs e)
- {
- //调用 ServerSendMsg方法 发送信息到客户端
- ServerSendMsg(txtSendMsg.Text.Trim());
- }
- //快捷键 Enter 发送信息
- private void txtSendMsg_KeyDown(object sender, KeyEventArgs e)
- {
- //如果用户按下了Enter键
- if (e.KeyCode == Keys.Enter)
- {
- //则调用 服务器向客户端发送信息的方法
- ServerSendMsg(txtSendMsg.Text.Trim());
- }
- }
- ///
- /// 获取当前系统时间的方法
- ///
- /// 当前时间
- private DateTime GetCurrentTime()
- {
- DateTime currentTime = new DateTime();
- currentTime = DateTime.Now;
- return currentTime;
- }
- }
- }
界面设计 - 客户端
IP文本框 name: txtIP Port文本框 name: txtPort 聊天内容文本框 name:txtMsg 发送信息文本框 name: txtCMsg
连接到服务端按钮 name: btnBeginListen 发送消息按钮 name: btnSend
客户端代码:
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Drawing;
- using System.Linq;
- using System.Text;
- using System.Windows.Forms;
- using System.Net.Sockets;
- using System.Threading;
- using System.Net;
- namespace ChatClient
- {
- public partial class FClient : Form
- {
- public FClient()
- {
- InitializeComponent();
- //关闭对文本框的非法线程操作检查
- TextBox.CheckForIllegalCrossThreadCalls = false;
- }
- //创建 1个客户端套接字 和1个负责监听服务端请求的线程
- Socket socketClient = null;
- Thread threadClient = null;
- private void btnBeginListen_Click(object sender, EventArgs e)
- {
- //定义一个套字节监听 包含3个参数(IP4寻址协议,流式连接,TCP协议)
- socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
- //需要获取文本框中的IP地址
- IPAddress ipaddress = IPAddress.Parse(txtIP.Text.Trim());
- //将获取的ip地址和端口号绑定到网络节点endpoint上
- IPEndPoint endpoint = new IPEndPoint(ipaddress, int.Parse(txtPort.Text.Trim()));
- //这里客户端套接字连接到网络节点(服务端)用的方法是Connect 而不是Bind
- socketClient.Connect(endpoint);
- //创建一个线程 用于监听服务端发来的消息
- threadClient = new Thread(RecMsg);
- //将窗体线程设置为与后台同步
- threadClient.IsBackground = true;
- //启动线程
- threadClient.Start();
- }
- /// <summary>
- /// 接收服务端发来信息的方法
- /// </summary>
- private void RecMsg()
- {
- while (true) //持续监听服务端发来的消息
- {
- //定义一个1M的内存缓冲区 用于临时性存储接收到的信息
- byte[] arrRecMsg = new byte[1024 * 1024];
- //将客户端套接字接收到的数据存入内存缓冲区, 并获取其长度
- int length = socketClient.Receive(arrRecMsg);
- //将套接字获取到的字节数组转换为人可以看懂的字符串
- string strRecMsg = Encoding.UTF8.GetString(arrRecMsg, 0, length);
- //将发送的信息追加到聊天内容文本框中
- txtMsg.AppendText("So-flash:" + GetCurrentTime() + "\r\n" + strRecMsg + "\r\n");
- }
- }
- /// <summary>
- /// 发送字符串信息到服务端的方法
- /// </summary>
- /// <param name="sendMsg">发送的字符串信息</param>
- private void ClientSendMsg(string sendMsg)
- {
- //将输入的内容字符串转换为机器可以识别的字节数组
- byte[] arrClientSendMsg = Encoding.UTF8.GetBytes(sendMsg);
- //调用客户端套接字发送字节数组
- socketClient.Send(arrClientSendMsg);
- //将发送的信息追加到聊天内容文本框中
- txtMsg.AppendText("天之涯:" + GetCurrentTime() + "\r\n" + sendMsg + "\r\n");
- }
- //点击按钮btnSend 向服务端发送信息
- private void btnSend_Click(object sender, EventArgs e)
- {
- //调用ClientSendMsg方法 将文本框中输入的信息发送给服务端
- ClientSendMsg(txtCMsg.Text.Trim());
- }
- //快捷键 Enter发送信息
- private void txtCMsg_KeyDown(object sender, KeyEventArgs e)
- {
- //当光标位于文本框时 如果用户按下了键盘上的Enter键
- if (e.KeyCode == Keys.Enter)
- {
- //则调用客户端向服务端发送信息的方法
- ClientSendMsg(txtCMsg.Text.Trim());
- }
- }
- /// <summary>
- /// 获取当前系统时间的方法
- /// </summary>
- /// <returns>当前时间</returns>
- private DateTime GetCurrentTime()
- {
- DateTime currentTime = new DateTime();
- currentTime = DateTime.Now;
- return currentTime;
- }
- }
- }
运行方法
获取电脑本机IP的方法: 例如:本机IP:192.168.0.3(可能变动) 端口号port可以随便写:1-65535之间的任意整数都行
1.打开程序 点击运行
2.在运行栏里输入cmd指令
3.输入查看IP指令: ipconfig
4.获取当前IP: 192.168.0.3. 当然不同的地方 本机IP有可能不一样
程序运行展示:
首先 点击服务端的 启动服务按钮 聊天内容出现"开始监听客户端传来的信息!"
然后 点击客户端上的"连接到服务端"按钮 可以看见服务端上又出现了一行字 "客户端连接成功"
之后 便可以 两端进行通信了
这样一个简单的聊天程序就完成了~~~~:)
源代码下载
客户端下载 ChatClient.zip 服务端下载 ChatServer.zip
[第二篇-文件发送]
程序简介
基于网友的提议,最近有点时间,便打算给之前的聊天程序增加一个功能-文件发送.
原理
文件发送跟字符串信息发送的原理其实是一样的,都是通过将需要发送的数据转换成计算机可以识别的字节数组来发送.当然,计算机本身并不知道你发送的是字符串信息还是文件,所以我们首先需要告诉计算机哪个发送的是文件,哪个是字符串信息;这里分别给它们的字节数组附加了一个类型标识符:字符串信息的字节数组标识符为0,文件的字节数组标识符为1.当一端将文件发送过去后,另一端则首先判断发送过来的类型标识符(1或者0),然后再调用相应的方法将获取的字节数组转换成人可以看懂的字符串信息或文件.
界面设计 - 客户端
这里新增了3个控件,用于实现文件发送功能.
Textbox: 文件名name: txtFileName
Button: 选择文件name: btnSelectFile 发送文件name: btnSendFile
代码实施 - 客户端
首先,我们需要写一个选择发送文件的方法,这里使用了最常见OpenFileDialog方法,用于选取需要发送的文件.
- string filePath = null; //文件的全路径
- string fileName = null; //文件名称(不包含路径)
- //选择要发送的文件
- private void btnSelectFile_Click(object sender, EventArgs e)
- {
- OpenFileDialog ofDialog = new OpenFileDialog();
- if (ofDialog.ShowDialog(this) == DialogResult.OK)
- {
- fileName = ofDialog.SafeFileName; //获取选取文件的文件名
- txtFileName.Text = fileName; //将文件名显示在文本框上
- filePath = ofDialog.FileName; //获取包含文件名的全路径
- }
- }
选取文件之后,通过FileStream来读取文件字节数组,然后在读到的文件字节数组的索引为0的位置上增加了一个文件标识符1,目的是告知计算机该字节数组为文件字节数组.这里在向服务端发送文件的同时也发送了一个文件名(字符串信息),目的是在服务端成功接收文件后,自动将原文件名附加上去.
- /// <summary>
- /// 发送文件的方法
- /// </summary>
- /// <param name="fileFullPath">文件全路径(包含文件名称)</param>
- private void SendFile(string fileFullPath)
- {
- if (fileFullPath == null)
- {
- MessageBox.Show("请选择需要发送的文件!");
- return;
- }
- else if (fileFullPath != null)
- {
- //创建文件流
- FileStream fs = new FileStream(fileFullPath, FileMode.Open);
- //创建一个内存缓冲区 用于临时存储读取到的文件字节数组
- byte[] arrClientFile = new byte[10 * 1024 * 1024];
- //从文件流中读取文件的字节数组 并将其存入到缓冲区arrClientFile中
- int realLength = fs.Read(arrClientFile, 0, arrClientFile.Length); //realLength 为文件的真实长度
- byte[] arrClientSendedFile = new byte[realLength + 1];
- //给新增标识符(实际要发送的)字节数组的索引为0的位置上增加一个标识符1
- arrClientSendedFile[0] = 1; //告诉机器该发送的字节数组为文件
- //将真实的文件字节数组完全拷贝到需要发送的文件字节数组中,从索引为1的位置开始存放,存放的字节长度为realLength.
- //实际发送的文件字节数组 arrSendedFile包含了2部分 索引为0位置上的标识符1 以及 后面的真实文件字节数组
- Buffer.BlockCopy(arrClientFile, 0, arrClientSendedFile, 1, realLength);
- //调用发送信息的方法 将文件名发送出去
- ClientSendMsg(fileName);
- socketClient.Send(arrClientSendedFile);
- txtMsg.AppendText("天之涯:" + GetCurrentTime() + "\r\n您发送了文件:" + fileName + "\r\n");
- }
- }
代码实施 - 服务端
由于新增了一个类型标识符,这里便将之前服务端接收信息的方法稍微改了下. 当服务端接收到含有标识符为0的字节数组,则直接将字节数组转换成字符串,并附加到聊天信息文本框上.若接收到的字节数组含有标识符1(即文件),则调用保存文件的方法SaveFile()将其保存为原文件;
- string strSRecMsg = null;
- /// <summary>
- /// 接收客户端发来的信息
- /// </summary>
- /// <param name="socketClientPara">客户端套接字的委托对象</param>
- private void ServerRecMsg(object socketClientPara)
- {
- Socket socketServer = socketClientPara as Socket;
- while (true)
- {
- //创建一个接收用的内存缓冲区 大小为10M字节数组
- byte[] arrServerRecMsg = new byte[10 * 1024 * 1024];
- //获取接收的数据,并存入内存缓冲区 返回一个字节数组的长度
- int length = socketServer.Receive(arrServerRecMsg);
- //判断发送过来的数据是文件还是普通文字信息
- if (arrServerRecMsg[0] == 0) //0为文字信息
- {
- //将字节数组 转换为人可以读懂的字符串
- strSRecMsg = Encoding.UTF8.GetString(arrServerRecMsg, 1, length - 1);//真实有用的文本信息要比接收到的少1(标识符)
- //将接收到的信息附加到文本框txtMsg上
- txtMsg.AppendText("天之涯:" + GetCurrentTime() + "\r\n" + strSRecMsg + "\r\n");
- }
- //如果发送过来的数据是文件
- if (arrServerRecMsg[0] == 1)
- {
- SaveFile(arrServerRecMsg, length - 1);//同样实际文件长度需要-1(减去标识符)
- }
- }
- }
SaveFile()方法里包含了FileStream的Write()方法,用于将接收到的文件字节数组保存为实际文件,这里Write()方法传入了3个参数,文件的字节数组,需要拷贝文件字节数组的初始位置以及拷贝的字节数组的长度[具体介绍可以看这里].在获取到文件的同时,这里也获取了文件名(字符串信息),用于附加到另存为对话框的文件名上;同时截取了文件名的后缀,作为需要保存的文件类型.最后,在文件成功保存到服务端所在的计算机的同时,在聊天内容文本框中附加了成功接收的文件名和文件的保存路径.
- /// <summary>
- /// 保存接收文件的方法 包含一个字节数组参数 和 文件的长度
- /// </summary>
- /// <param name="arrFile">字节数组参数</param>
- /// <param name="fileLength">文件的长度</param>
- private void SaveFile(byte[] arrFile, int fileLength)
- {
- //创建一个用于保存文件的对话框
- SaveFileDialog sfDialog = new SaveFileDialog();
- //获取文件名的后缀 比如文本文件后缀 .txt
- string fileNameSuffix = strSRecMsg.Substring(strSRecMsg.LastIndexOf("."));
- sfDialog.Filter = "(*" + fileNameSuffix + ")|*" + fileNameSuffix + ""; //文件类型
- sfDialog.FileName = strSRecMsg; //文件名
- //如果点击了对话框中的保存文件按钮
- if (sfDialog.ShowDialog(this) == DialogResult.OK)
- {
- string savePath = sfDialog.FileName; //获取文件的全路径
- //保存文件
- FileStream fs = new FileStream(savePath, FileMode.Create);
- fs.Write(arrFile, 1, fileLength);
- string fName = savePath.Substring(savePath.LastIndexOf("\\") + 1); //文件名 不带路径
- string fPath = savePath.Substring(0, savePath.LastIndexOf("\\")); //文件路径 不带文件名
- txtMsg.AppendText("SoFlash:" + GetCurrentTime() + "\r\n您成功接收了文件" + fName + "\r\n保存路径为:" + fPath + "\r\n");
- }
- }
运行程序
首先,启动服务端并持续监听客户端对其的连接,当客户端成功连接上服务端之后,两端便可以开始通信了.
两端建立连接之后,便可以开始互相通信了.
简单的两端对聊之后, 本人便打算发送个文件过去.
选取了一本张道真的语法书,后缀为.pdf(文件类型)
当点击发送文件按钮后,客户端聊天内容中显示"您发送了文件:张道真实用英语语法.pdf".
这时服务端收到文件后,程序弹出一个另存为对话框,用于保存接收到的文件.这里我们可以看到系统自动附加上了文件名和保存类型.
当服务端用户接收并保存文件之后,聊天内容里显示"您成功接收了文件张道真实用英语语法.pdf" 以及文件的保存路径.
附上源代码
服务端 ChatServer2.zip 客户端 ChatClient2.zip
[第三篇-信息群发]
程序简介
好久没写博客了,最近时间比较充足.于是便打算把之前的聊天程序功能改进下,增加了一个服务端给客户端群发信息的功能.
原理
首先我们需要获取连接客户端的IP和Port,并添加到客户端列表里作为每个客户端的唯一标识.然后通过相应客户端的Socket.Send()方法将信息发送出去.
服务端给客户端的信息群发与服务端给单个客户端的信息发送原理是一样的,通过遍历客户端列表里的所有客户端标识,然后把信息一个个发送出去.
界面设计 - 客户端
界面设计 - 服务端
代码实施 - 客户端
客户端没有做什么功能改进,这里就不贴代码了,需要看的学友可以到随笔后面下载源代码.
代码实施 - 服务端
这里将手动输入服务端IPv4地址改为了程序自动获取
- /// <summary>
- /// 获取本地IPv4地址
- /// </summary>
- /// <returns>本地IPv4地址</returns>
- public IPAddress GetLocalIPv4Address()
- {
- IPAddress localIPv4 = null;
- //获取本机所有的IP地址列表
- IPAddress[] ipAddressList = Dns.GetHostAddresses(Dns.GetHostName());
- foreach (IPAddress ipAddress in ipAddressList)
- {
- //判断是否是IPv4地址
- if (ipAddress.AddressFamily == AddressFamily.InterNetwork) //AddressFamily.InterNetwork表示IPv4
- {
- localIPv4 = ipAddress;
- }
- else
- continue;
- }
- return localIPv4;
- }
为了方便后期给所有访问的客户端群发信息,我们需要用过通过监听客户端来获取所有访问客户端的IP地址和端口号,并组成每个访问客户端的唯一标识clientName 用于显示在客户端列表上;客户端唯一标识还有个作用就是服务端可以选择性的给单独某个客户端发送信息.
- //用于保存所有通信客户端的Socket
- Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>();
- //创建与客户端建立连接的套接字
- Socket socConnection = null;
- string clientName = null; //创建访问客户端的名字
- IPAddress clientIP; //访问客户端的IP
- int clientPort; //访问客户端的端口号
- /// <summary>
- /// 持续不断监听客户端发来的请求, 用于不断获取客户端发送过来的连续数据信息
- /// </summary>
- private void WatchConnecting()
- {
- while (true)
- {
- try
- {
- socConnection = socketWatch.Accept();
- }
- catch (Exception ex)
- {
- txtMsg.AppendText(ex.Message); //提示套接字监听异常
- break;
- }
- //获取访问客户端的IP
- clientIP = (socConnection.RemoteEndPoint as IPEndPoint).Address;
- //获取访问客户端的Port
- clientPort = (socConnection.RemoteEndPoint as IPEndPoint).Port;
- //创建访问客户端的唯一标识 由IP和端口号组成
- clientName = "IP: " + clientIP +" Port: "+ clientPort;
- lstClients.Items.Add(clientName); //在客户端列表添加该访问客户端的唯一标识
- dicSocket.Add(clientName, socConnection); //将客户端名字和套接字添加到添加到数据字典中
- //创建通信线程
- ParameterizedThreadStart pts = new ParameterizedThreadStart(ServerRecMsg);
- Thread thread = new Thread(pts);
- thread.IsBackground = true;
- //启动线程
- thread.Start(socConnection);
- txtMsg.AppendText("IP: " + clientIP + " Port: " + clientPort + " 的客户端与您连接成功,现在你们可以开始通信了...\r\n");
- }
- }
服务端向客户端发送信息,在没有选择具体某个客户端的情况下,默认群发. 如果选择了具体某个客户端,则单独向该客户端发送信息.
- /// <summary>
- /// 发送信息到客户端的方法
- /// </summary>
- /// <param name="sendMsg">发送的字符串信息</param>
- private void ServerSendMsg(string sendMsg)
- {
- sendMsg = txtSendMsg.Text.Trim();
- //将输入的字符串转换成 机器可以识别的字节数组
- byte[] arrSendMsg = Encoding.UTF8.GetBytes(sendMsg);
- //向客户端列表选中的客户端发送信息
- if (!string.IsNullOrEmpty(lstClients.Text.Trim()))
- {
- //获得相应的套接字 并将字节数组信息发送出去
- dicSocket[lstClients.Text.Trim()].Send(arrSendMsg);
- //通过Socket的send方法将字节数组发送出去
- txtMsg.AppendText("您在 " + GetCurrentTime() + " 向 IP: " + clientIP + " Port: " + clientPort + " 的客户端发送了:\r\n" + sendMsg + "\r\n");
- }
- else //如果未选择任何客户端 则默认为群发信息
- {
- //遍历所有的客户端
- for (int i = 0; i < lstClients.Items.Count; i++)
- {
- dicSocket[lstClients.Items[i].ToString()].Send(arrSendMsg);
- }
- txtMsg.AppendText("您在 " + GetCurrentTime() + " 群发了信息:\r\n" + sendMsg + " \r\n");
- }
- }
运行程序
1.首先启动服务端并连接各个客户端
2.服务端向所有客户端群发信息
3.向指定的客户端发送信息
这样,服务端向客户端群发信息的功能就做好了.如果大家感兴趣,可以结合第2篇-文件发送 来做个服务端向客户端群发文件的功能 :)
附上源代码
服务端ChatServer3.zip 客户端 ChatClient3.zip