聊天室(WinForm)"/>
.NET编程——利用C#实现基于Socket类的聊天室(WinForm)
目录
- 引言
- 数据库设计
- 登录注册
- 窗体设计
- 控件选择
- 程序设计
- step1-连接数据库
- step2-注册
- step3-登录
- 源码
- 创建聊天室(服务器)
- 窗体设计
- 控件选择
- 程序设计
- step1-定义变(常)量
- step2-创建服务器并监听客户端连接
- step3-接受连接回调
- step4-接受消息
- 源码
- 创建用户(客户端)
- 窗体设计
- 控件选择
- 程序设计
- step1-定义变(常)量
- step2-权限设置并重置账户
- step3-更新在线用户
- step4-写入历史聊天记录
- step5-连接服务器
- step6-接受消息回调
- step7-发送消息与显示消息
- step8-清空聊天区
- step9-保存聊天记录
- 源码
- 结尾与展示
在学习C#和MySQL实现注册登录和TCP协议的Socket通信后,本文将介绍如何利用Socket类中的异步通信函数来实现本地聊天室功能,
引言
Socket通信限制了客户端与客户端之间的通信,客户端只能接收来自服务器的消息而不能接收到客户端发送的消息,因此服务器最佳的选择是起到一个中转的作用,A客户端发送消息给服务器,服务器将接收到的A客户端发来的消息发送给B客户端,当然也可以是某部分客户端或者全部客户端,所有的客户端都要和这个服务器建立连接,服务器和客户端都只有两个功能——发送接收消息。
数据库设计
具体操作步骤可参考.NET编程——利用C#和MySQL实现注册登录(WinForm)中的数据库设计部分,这一部分的主要目的在于创建一个本地数据库用于写入和读取账号、密码与在线状态。
登录注册
窗体设计
可输入账号密码,点击按键即可注册、登录。
控件选择
根据窗体设计选择相应的控件,包含按键、文本框以及标签等。
其中TableLayoutPanel控件用于调整窗体控件的布局,具体可看C#控件自适应布局。
程序设计
step1-连接数据库
static string connStr = @"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=D:\Code\WeTalk\WeTalk\WeTalkDataBase.mdf;Integrated Security=True"; //连接数据库标识
using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类
{conn.Open(); //打开数据库连接/* 执行内容 */conn.Close(); //关闭数据库连接
}
step2-注册
/* 点击注册按键时触发 */
private void btnSign_Click(object sender, EventArgs e)
{/* 获取控件里的值 */string account = txtAccount.Text;string password = txtPassword.Text;if (account == "" || password == ""){MessageBox.Show("请输入完整");clearText(); //清空文本return;}try{string sqlSelect = string.Format("select count(*) from [User] where Account='{0}'", account); //SQL语句,选择表User中Account为account的行/// 创建对象时使用using可以在使用完该对象后,自动释放资源using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdSelect = new SqlCommand(sqlSelect, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接if ((int)cmdSelect.ExecuteScalar() > 0){conn.Close(); //关闭数据库连接MessageBox.Show("用户已存在!");clearText(); //清空文本}else{if (account == "admin" && password != "admin"){conn.Close(); //关闭数据库连接MessageBox.Show("注册失败");clearText(); //清空文本return;}string sqlInsert = string.Format("insert into [User] (Account,Password,IsOnline)values('{0}','{1}',{2})",account, password, 0);,//SQL语句,将参数Name、Password、IsOnline插入到表User中SqlCommand cmdInsert = new SqlCommand(sqlInsert, conn);cmdInsert.ExecuteNonQuery(); //执行SQL语句conn.Close(); //关闭数据库连接MessageBox.Show("注册成功");clearText(); //清空文本}}}}catch (Exception ex) //异常捕获{MessageBox.Show(ex.ToString()); //显示异常信息}
}
step3-登录
/* 点击登录按键时触发 */
private void btnLogin_Click(object sender, EventArgs e)
{/* 获取控件里的值 */string account = txtAccount.Text;string password = txtPassword.Text;if (account == "" || password == ""){MessageBox.Show("请输入完整!");clearText(); //清空文本return;}string sqlSelect = string.Format("select Password,IsOnline from [User] where Account='{0}'", account); //SQL语句,选择PassWord和IsOnline/* 创建对象时使用using可以在使用完该对象后,自动释放资源 */using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdSelect = new SqlCommand(sqlSelect, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接SqlDataReader sqlRead = cmdSelect.ExecuteReader(); //遍历数据库//cmdSelect.ExecuteScalar(); //执行SQL语句if (!sqlRead.Read()){conn.Close(); //关闭数据库连接MessageBox.Show("账号不存在!请重新输入");clearText(); //清空文本}else if (sqlRead["IsOnline"].ToString().Trim() == "1" && account != "admin"){conn.Close(); //关闭数据库连接MessageBox.Show("账号已登录!请重新输入");clearText(); //清空文本}else if (sqlRead["Password"].ToString().Trim() == password || (account == "admin" && password == "admin")){conn.Close(); //关闭数据库连接MessageBox.Show("登录成功!");string sqlUpdate = string.Format("update [User] set IsOnline={0} where Account='{1}'", 1, account); //SQL语句,更新IsOnline为上线状态using (SqlCommand cmdUpdate = new SqlCommand(sqlUpdate, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接//执行非查询命令时使用ExecuteNonQuery,会返回影响的行数cmdUpdate.ExecuteNonQuery(); //执行SQL语句conn.Close(); //关闭数据库连接clearText(); //清空文本FormTalk formTalk = new FormTalk(account, this); //跨窗体传入参数this.Hide(); //隐藏当前窗体formTalk.ShowDialog(); //显示聊天窗体}}else{conn.Close(); //关闭数据库连接MessageBox.Show("密码错误!请重新输入");clearText();}}}
}
源码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;namespace Client
{public partial class FormLogin : Form{// 连接数据库标示const string connStr = @"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=D:\Code\WeTalk\WeTalk\WeTalkDataBase.mdf;Integrated Security=True"; //连接数据库标识public FormLogin(){InitializeComponent();}/* 打开窗体时触发 */private void FormLogin_Load(object sender, EventArgs e){clearText();//clearDataBase();}/* 清空数据库函数 */private void clearDataBase(){string sqlDelete = "delete from [User]"; //SQL语句,清空表User内容using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdDelete = new SqlCommand(sqlDelete, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接//执行非查询命令时使用ExecuteNonQuery,会返回影响的行数cmdDelete.ExecuteNonQuery(); //执行SQL语句conn.Close(); //关闭数据库连接MessageBox.Show("管理员重置了所有账号");}}}/* 清空输入框内容函数 */private void clearText(){/* Action委托修改控件内容 */this.Invoke(new Action(() =>{txtAccount.Text = "";txtPassword.Text = "";}));}/* 点击注册按键时触发 */private void btnSign_Click(object sender, EventArgs e){/* 获取控件里的值 */string account = txtAccount.Text;string password = txtPassword.Text;if (account == "" || password == ""){MessageBox.Show("请输入完整");clearText(); //清空文本return;}try{string sqlSelect = string.Format("select count(*) from [User] where Account='{0}'", account); //SQL语句,选择表User中Account为account的行/// 创建对象时使用using可以在使用完该对象后,自动释放资源using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdSelect = new SqlCommand(sqlSelect, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接if ((int)cmdSelect.ExecuteScalar() > 0){conn.Close(); //关闭数据库连接MessageBox.Show("用户已存在!");clearText(); //清空文本}else{if (account == "admin" && password != "admin"){conn.Close(); //关闭数据库连接MessageBox.Show("注册失败");clearText(); //清空文本return;}string sqlInsert = string.Format("insert into [User] (Account,Password,IsOnline)values('{0}','{1}',{2})",account, password, 0);//SQL语句,将参数Name、Password、IsOnline插入到表User中SqlCommand cmdInsert = new SqlCommand(sqlInsert, conn);cmdInsert.ExecuteNonQuery(); //执行SQL语句conn.Close(); //关闭数据库连接MessageBox.Show("注册成功");clearText(); //清空文本}}}}catch (Exception ex) //异常捕获{MessageBox.Show(ex.ToString()); //显示异常信息}}/* 点击登录按键时触发 */private void btnLogin_Click(object sender, EventArgs e){/* 获取控件里的值 */string account = txtAccount.Text;string password = txtPassword.Text;if (account == "" || password == ""){MessageBox.Show("请输入完整!");clearText(); //清空文本return;}string sqlSelect = string.Format("select Password,IsOnline from [User] where Account='{0}'", account); //SQL语句,选择PassWord和IsOnline/* 创建对象时使用using可以在使用完该对象后,自动释放资源 */using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdSelect = new SqlCommand(sqlSelect, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接SqlDataReader sqlRead = cmdSelect.ExecuteReader(); //遍历数据库//cmdSelect.ExecuteScalar(); //执行SQL语句if (!sqlRead.Read()){conn.Close(); //关闭数据库连接MessageBox.Show("账号不存在!请重新输入");clearText(); //清空文本}else if (sqlRead["IsOnline"].ToString().Trim() == "1" && account != "admin"){conn.Close(); //关闭数据库连接MessageBox.Show("账号已登录!请重新输入");clearText(); //清空文本}else if (sqlRead["Password"].ToString().Trim() == password || (account == "admin" && password == "admin")){conn.Close(); //关闭数据库连接MessageBox.Show("登录成功!");string sqlUpdate = string.Format("update [User] set IsOnline={0} where Account='{1}'", 1, account); //SQL语句,更新IsOnline为上线状态using (SqlCommand cmdUpdate = new SqlCommand(sqlUpdate, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接//执行非查询命令时使用ExecuteNonQuery,会返回影响的行数cmdUpdate.ExecuteNonQuery(); //执行SQL语句conn.Close(); //关闭数据库连接clearText(); //清空文本FormTalk formTalk = new FormTalk(account, this); //跨窗体传入参数this.Hide(); //隐藏当前窗体formTalk.ShowDialog(); //显示聊天窗体}}else{conn.Close(); //关闭数据库连接MessageBox.Show("密码错误!请重新输入");clearText();}}}}}
}
创建聊天室(服务器)
服务器起中转作用,无需输入发送的消息,将接受到的消息原封不动的返回给所有客户端即可。
窗体设计
显示服务器的IP与端口号。
控件选择
根据窗体设计选择文本框控件。
其中TableLayoutPanel控件用于调整窗体控件的布局,具体可看C#控件自适应布局。
程序设计
step1-定义变(常)量
#region 变(常)量定义
/* buffer:存放接受消息缓存的数组* listSocketWorker:存放套接字的列表* MAXVALUE:服务器最大连接数* PORT:服务器的网络终结点,即IP:端口号* socketWatch:监听客户端连接的服务器套接字*/
byte[] buffer = new byte[1024 * 1024]; //创建接受消息缓存数组并约定缓存长度解决粘包问题
private List<Socket> listSocketWorker = new List<Socket>(); //创建列表存储套接字
const int MAXVALUE = 10; //设定最大连接数
const int PORT = 8888; //设定端口号
private Socket socketServer = null; //创建全局变量服务器套接字
#endregion
step2-创建服务器并监听客户端连接
创建了一个btnOpen_Click()委托函数,后续可以直接或者利用委托触发创建服务器。
/* 窗体打开时触发 */
private void FormOpen_Load(object sender, EventArgs e)
{btnOpen_Click(this, null); //触发打开服务器按键
}
/* 点击打开按键时触发 */
private void btnOpen_Click(object sender, EventArgs e)
{socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //实例化TCP通信协议Socket/* 监听客户端 */IPAddress _ip = IPAddress.Any; //监听所有活动网卡IPEndPoint _port = new IPEndPoint(_ip, PORT); //创建监听对象try{socketServer.Bind(_port); //开始监听socketServer.Listen(10); //服务器最大连接数为10socketServer.BeginAccept(new AsyncCallback(AcceptCallback), socketServer); //异步接受连接///参数1:回调函数,参数2:传入回调函数的参数}catch //异常捕获{MessageBox.Show("服务器已上线");}
}
step3-接受连接回调
利用Socket类中的BeginAccept()函数开始接受客户端的连接,此时会自动生成一个线程去接受客户端的连接,接受到客户端的连接后返回请求的对象给回调函数。
/* 接受连接异步回调处理 */
private void AcceptCallback(IAsyncResult ia)
{socketServer = ia.AsyncState as Socket; //将传入回调函数的参数实例化为Socket类Socket socketWorker = socketServer.EndAccept(ia); //连接成功并返回一个套接字用于通信if (socketWorker != null){/* Action委托修改lblState控件内容 */this.Invoke(new Action(() =>{lblState.Text = socketWorker.LocalEndPoint.ToString();}));}Receive(socketWorker);socketServer.BeginAccept(new AsyncCallback(AcceptCallback), socketServer); //再次异步接受连接/// 利用BeginAccept异步接受连接时会自动创建线程并自动结束线程
}
step4-接受消息
为了保证BeginReceive()函数在接受客户端的消息时再次接受到消息时接受消息缓存错误,利用线程锁保证消息被完整转发出去。
private void Receive(Socket socketWorker)
{if(!listSocketWorker.Contains(socketWorker))listSocketWorker.Add(socketWorker); //将套接字保存到列表中socketWorker.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), socketWorker); //异步接受消息/// 参数1:接受消息缓存,参数2:接受消息缓存偏倚,参数3:接受消息缓存长度,参数4:套接字行为,参数5:回调函数,参数6:传入回调函数的参数
}
/* 接受消息异步回调处理 */
private void ReceiveCallback(IAsyncResult ia)
{Socket socketWorker = ia.AsyncState as Socket; //将传入回调函数的参数实例化为Socket类try{int bytesRead = socketWorker.EndReceive(ia); //接受消息成功并返回消息长度if (bytesRead > 0) //接受到非空消息{string context = Encoding.Default.GetString(buffer, 0, bytesRead); //缓存解码为字符串socketWorker.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), socketWorker); //再次异步接受消息/// 利用BeginReceive异步接受消息时会自动创建线程并自动结束线程if (!String.IsNullOrEmpty(context)){/* 线程锁——完成代码块后执行其他线程 */lock (socketWorker){foreach (var item in listSocketWorker) // 接受消息后消息转发给所有客户端item.Send(buffer, 0, bytesRead, SocketFlags.None);}}}}catch //异常捕获{MessageBox.Show("客户端已下线");listSocketWorker.Remove(socketWorker);}
}
源码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;namespace Server
{public partial class FormOpen : Form{#region 变(常)量定义/* buffer:存放接受消息缓存的数组* listSocketWorker:存放套接字的列表* MAXVALUE:服务器最大连接数* PORT:服务器的网络终结点,即IP:端口号* socketWatch:监听客户端连接的服务器套接字*/byte[] buffer = new byte[1024 * 1024]; //创建接受消息缓存数组并约定缓存长度解决粘包问题private List<Socket> listSocketWorker = new List<Socket>(); //创建列表存储套接字const int MAXVALUE = 10; //设定最大连接数const int PORT = 8888; //设定端口号private Socket socketServer = null; //创建全局变量服务器套接字#endregionpublic FormOpen(){InitializeComponent();}/* 窗体打开时触发 */private void FormOpen_Load(object sender, EventArgs e){btnOpen_Click(this, null); //触发打开服务器按键}/* 点击打开按键时触发 */private void btnOpen_Click(object sender, EventArgs e){socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //实例化TCP通信协议Socket/* 监听客户端 */IPAddress _ip = IPAddress.Any; //监听所有活动网卡IPEndPoint _port = new IPEndPoint(_ip, PORT); //创建监听对象try{socketServer.Bind(_port); //开始监听socketServer.Listen(10); //服务器最大连接数为10socketServer.BeginAccept(new AsyncCallback(AcceptCallback), socketServer); //异步接受连接///参数1:回调函数,参数2:传入回调函数的参数}catch //异常捕获{MessageBox.Show("服务器已上线");}}/* 接受连接异步回调处理 */private void AcceptCallback(IAsyncResult ia){socketServer = ia.AsyncState as Socket; //将传入回调函数的参数实例化为Socket类Socket socketWorker = socketServer.EndAccept(ia); //连接成功并返回一个套接字用于通信if (socketWorker != null){/* Action委托修改lblState控件内容 */this.Invoke(new Action(() =>{lblState.Text = socketWorker.LocalEndPoint.ToString();}));}Receive(socketWorker);socketServer.BeginAccept(new AsyncCallback(AcceptCallback), socketServer); //再次异步接受连接/// 利用BeginAccept异步接受连接时会自动创建线程并自动结束线程}private void Receive(Socket socketWorker){if(!listSocketWorker.Contains(socketWorker))listSocketWorker.Add(socketWorker); //将套接字保存到列表中socketWorker.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), socketWorker); //异步接受消息/// 参数1:接受消息缓存,参数2:接受消息缓存偏倚,参数3:接受消息缓存长度,参数4:套接字行为,参数5:回调函数,参数6:传入回调函数的参数}/* 接受消息异步回调处理 */private void ReceiveCallback(IAsyncResult ia){Socket socketWorker = ia.AsyncState as Socket; //将传入回调函数的参数实例化为Socket类try{int bytesRead = socketWorker.EndReceive(ia); //接受消息成功并返回消息长度if (bytesRead > 0) //接受到非空消息{string context = Encoding.Default.GetString(buffer, 0, bytesRead); //缓存解码为字符串socketWorker.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), socketWorker); //再次异步接受消息/// 利用BeginReceive异步接受消息时会自动创建线程并自动结束线程if (!String.IsNullOrEmpty(context)){/* 线程锁——完成代码块后执行其他线程 */lock (socketWorker){foreach (var item in listSocketWorker) // 接受消息后消息转发给所有客户端item.Send(buffer, 0, bytesRead, SocketFlags.None);}}}}catch //异常捕获{MessageBox.Show("客户端已下线");listSocketWorker.Remove(socketWorker);}}private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e){}}
}
创建用户(客户端)
既然是聊天室,当然能显示在线的用户。同时,在退出聊天室时可保存聊天记录。当聊天区内容过多时清空聊天区内。最后,管理员可以重置聊天室用户的账号与密码。
窗体设计
可显示在线用户和聊天内容,点击按键即可发送消息,重置账号与密码。
控件选择
根据窗体设计选择相应的控件,包含按键、文本框、标签以及菜单栏等。
其中,菜单栏的设置在聊天区内,故此在txtTalk控件属性中的ContextMenuStrip需选择设计好的菜单栏,如下图所示:
程序设计
step1-定义变(常)量
定义_ip时需要根据活跃网卡的IP来设置,端口号与服务器的端口号保持一致即可。
#region 变(常)量定义
/* _ip:服务器IP* _path:存放聊天记录的路径* buffer:存放接受消息缓存的数组* connStr:连接数据库标示* lastForm:存放上级窗体* PORT:服务器的网络终结点,即IP:端口号* socketClient:与服务器通信的客户端套接字* strAccount:用户名字符串* thUpdateUser:用于更新在线用户的线程*/
IPAddress _ip = IPAddress.Parse("10.102.101.212"); //IPAddress包含了一个IP地址,IPEndPoin包含了一对IP地址和端口
//IPAddress _ip = IPAddress.Parse("192.168.250.66"); //IPAddress包含了一个IP地址,IPEndPoin包含了一对IP地址和端口
string _path = System.AppDomain.CurrentDomain.BaseDirectory + @"聊天记录\"; //设置聊天记录保存路径
byte[] buffer = new byte[5 * 1024 * 1024]; //创建接受消息缓存数组并约定缓存长度解决粘包问题
const string connStr = @"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=D:\Code\WeTalk\WeTalk\WeTalkDataBase.mdf;Integrated Security=True"; //连接数据库标识
Form lastForm; //上级窗体
const int PORT = 8888; //设定端口号
Socket socketClient = null; //创建全局变量客户端套接字
public string strAccount; //创建全局变量用户名
Thread thUpdateUser; //创建线程用于更新在线用户
#endregion
step2-权限设置并重置账户
管理员才可重置账户与密码,通过修改按键的Enabled属性来设置权限。
/* 窗体打开时触发 */
private void FormTalk_Load(object sender, EventArgs e)
{btnClean.Enabled = false;/* Action委托修改控件内容 */this.Invoke(new Action(() =>{lblAccount.Text = "用户名:" + this.strAccount; //显示用户名/* 权限设置 */if (strAccount == "admin")btnClean.Enabled = true;elsebtnClean.Enabled = false;}));/*省略…………省略*/
}
/* 点击重置按键时触发 */
private void btnClear_Click(object sender, EventArgs e)
{/* 清空数据库 */string sqlDelete = "delete from [User]"; //SQL语句,清空User内容using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdDelete = new SqlCommand(sqlDelete, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接//执行非查询命令时使用ExecuteNonQuery,会返回影响的行数cmdDelete.ExecuteNonQuery(); //执行SQL语句conn.Close(); //关闭数据库连接ShowMsg("管理员重置了所有账号");}}
}
step3-更新在线用户
创建一个线程去查询SQL表中在线的用户,用户在线时添加至listBox控件中去,用户不在线时从listBox控件中移除该用户。
/* 窗体打开时触发 */
private void FormTalk_Load(object sender, EventArgs e)
{/*省略…………省略*//* 线程更新在线用户 */thUpdateUser = new Thread(UpdateUser); //实例化线程thUpdateUser.IsBackground = true;thUpdateUser.Start(); //启动线程/*省略…………省略*/
}
/* 更新在线用户线程函数 */
void UpdateUser()
{while (true) //死循环{/* 更新在线对象 */string sqlSelect = string.Format("select Account,IsOnline from [User]"); //SQL语句,选择表User中Account和IsOnline/// 创建对象时使用using可以在使用完该对象后,自动释放资源using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdSelect = new SqlCommand(sqlSelect, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接SqlDataReader sqlRead = cmdSelect.ExecuteReader(); //遍历数据库while (sqlRead.Read()){int index = lstAccount.FindString(sqlRead["Account"].ToString()); //列表中某值的下标// 列表里没有 但 在线的if (index == -1 && (int)sqlRead["IsOnline"] == 1){/* Action委托修改lstAccount控件内容 */this.Invoke(new Action(() =>{lstAccount.Items.Add(sqlRead["Account"].ToString());}));}// 列表里有 但 不在线的else if (index != -1 && (int)sqlRead["IsOnline"] == 0){/* Action委托修改lstAccount控件内容 */this.Invoke(new Action(() =>{lstAccount.Items.RemoveAt(index);}));}} conn.Close(); //关闭数据库连接}}}}
step4-写入历史聊天记录
/* 窗体打开时触发 */
private void FormTalk_Load(object sender, EventArgs e)
{/*省略…………省略*/try{/*省略…………省略*//* 写入历史聊天记录 */string log_path = _path + strAccount + "_log.txt";if (File.Exists(log_path)){string[] lines = File.ReadAllLines(log_path);foreach (string line in lines){this.Invoke(new Action(() =>{txtTalk.AppendText(line + "\r\n");}));}}}/*省略…………省略*/
}
step5-连接服务器
/* 窗体打开时触发 */
private void FormTalk_Load(object sender, EventArgs e)
{/*省略…………省略*/try{socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //实例化TCP通信协议SocketIPEndPoint _port = new IPEndPoint(_ip, PORT); //创建连接对象socketClient.SendTimeout = 1000; //设置连接时限socketClient.BeginConnect(_port, new AsyncCallback(ConnectCallback), socketClient); //异步请求连接/// 参数1:服务器终结点,参数2:回调函数,参数3:传入回调函数的参数/*省略…………省略*/}catch { } //异常捕获
}
/* 发送连接异步回调处理 */
private void ConnectCallback(IAsyncResult ia)
{try{((Socket)ia.AsyncState).EndConnect(ia); //结束连接请求socketClient = ia.AsyncState as Socket; //将传入回调函数的参数实例化为Socket类byte[] sendBuffer = Encoding.Default.GetBytes(strAccount + ":上线了"); //缓存解码为字符串socketClient.Send(sendBuffer); //发送缓存至服务器socketClient.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), socketClient); //异步接受消息/// 参数1:接受消息缓存,参数2:接受消息缓存偏倚,参数3:接受消息缓存长度,参数4:套接字行为,参数5:回调函数,参数6:传入回调函数的参数}catch (Exception ex) //异常捕获{ShowMsg(ex.ToString());}
}
step6-接受消息回调
利用Socket类中的BeginReceive()函数接受服务器发来的消息,此时会自动生成一个线程去接受消息,接受到消息后返回请求的对象给回调函数,利用EndReceive()函数去结束接受消息并返回接受缓存长度。同时,利用线程锁保证接收到消息被完整的解码显示在聊天区。
/* 接受消息异步回调处理 */
private void ReceiveCallback(IAsyncResult ia)
{socketClient = ia.AsyncState as Socket; //将传入回调函数的参数实例化为Socket类if (socketClient == null){return;}try{int bytesRead = socketClient.EndReceive(ia); //接受消息成功并返回消息长度 if (bytesRead > 0) //接受到非空消息{string context = Encoding.Default.GetString(buffer, 0, bytesRead); //缓存解码为字符串socketClient.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), socketClient); //异步接受消息/// 利用BeginReceive异步接受消息时会自动创建线程并自动结束线程if (!String.IsNullOrEmpty(context)){/* 线程锁——完成代码块后执行其他线程 */lock (socketClient){string[] strContext = context.Split(':'); //解码字符串if (strContext.Length > 0 && strContext[0] != strAccount)ShowMsg(context);/// 服务器转接的消息不是用户自生发出的数据时才显示}}}}catch(Exception ex) //异常捕获{ShowMsg(ex.ToString());}
}
step7-发送消息与显示消息
/* 点击发送按键时触发 */
private void btnSend_Click(object sender, EventArgs e)
{try{/* 发送消息至服务器 */byte[] buffer = Encoding.Default.GetBytes(strAccount + ":" + txtType.Text); //将泛型转换为数组socketClient.Send(buffer, 0, buffer.Length, SocketFlags.None); //客户端发送消息至服务器/// 参数1:接受消息缓存,参数2:接受消息缓存偏倚,参数3:接受消息缓存长度,参数4:套接字行为/* Action委托修改控件内容 */this.Invoke(new Action(() =>{ShowMsg(strAccount + ":" + txtType.Text);txtType.Text = "";}));}catch(Exception ex){ShowMsg(ex.ToString());}
}
/* 显示消息函数 */
private void ShowMsg(string str)
{string time = DateTime.Now.ToString("G"); //获取当前时间(长日期/时间)/* Action委托修改txtTalk控件内容 */this.Invoke(new Action(() =>{txtTalk.AppendText(time + "\r\n" + str + "\r\n");}));
}
step8-清空聊天区
修改主线程中的控件内容必须通过多线程或者委托的方式来实现,本文通过EventHandler委托来触发Action委托修改控件内容,方法如下:
/* 点击右键菜单项时触发 */
private void contextMenuStrip_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
{contextMenuStrip.Items[0].Click += new EventHandler(clearTxtTalk); //定义EvendHandler委托
}
/* 聊天区记录清空函数 */
private void clearTxtTalk(object sender, EventArgs e)
{/* Action委托修改txtTalk控件内容 */this.Invoke(new Action(() =>{txtTalk.Text = "";}));
}
step9-保存聊天记录
/* 关闭窗体时触发 */
private void FormTalk_FormClosing(object sender, FormClosingEventArgs e)
{DialogResult res = MessageBox.Show("是否保存聊天记录?", "提示", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); //保存结果信息/// 参数1:显示文本,参数2:标题,参数3:按键类型,参数4:显示图标if (res == DialogResult.Yes){if (!System.IO.Directory.Exists(_path))System.IO.Directory.CreateDirectory(_path);/* 保存聊天记录 */string log_path = _path + strAccount + "_log.txt";using (FileStream logFileStream = new FileStream(log_path, FileMode.Create, FileAccess.Write)) //创建写入文件{using (StreamWriter logWriter = new StreamWriter(logFileStream)){logWriter.Write(txtTalk.Text);logWriter.Close();}logFileStream.Close();}e.Cancel = false; //确认关闭窗体}else if (res == DialogResult.No) //不保存聊天记录{e.Cancel = false; //确认关闭窗体}else //取消{e.Cancel = true; //取消关闭窗体}
}
/* 关闭窗体后触发 */
private void FormTalk_FormClosed(object sender, FormClosedEventArgs e)
{try{string sqlUpdate = string.Format("update [User] set IsOnline={0} where Account='{1}'", 0, strAccount); //SQL语句,更新isOnline为上线状态using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdUpdate = new SqlCommand(sqlUpdate, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接//执行非查询命令时使用ExecuteNonQuery,会返回影响的行数cmdUpdate.ExecuteNonQuery(); //执行SQL语句conn.Close(); //关闭数据库连接System.Environment.Exit(0); //杀死进程}}}catch { } //异常捕获
}
源码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;namespace Client
{public partial class FormTalk : Form{#region 变(常)量定义/* _ip:服务器IP* _path:存放聊天记录的路径* buffer:存放接受消息缓存的数组* connStr:连接数据库标示* lastForm:存放上级窗体* PORT:服务器的网络终结点,即IP:端口号* socketClient:与服务器通信的客户端套接字* strAccount:用户名字符串* thUpdateUser:用于更新在线用户的线程*/IPAddress _ip = IPAddress.Parse("10.102.101.212"); //IPAddress包含了一个IP地址,IPEndPoin包含了一对IP地址和端口//IPAddress _ip = IPAddress.Parse("192.168.250.66"); //IPAddress包含了一个IP地址,IPEndPoin包含了一对IP地址和端口string _path = System.AppDomain.CurrentDomain.BaseDirectory + @"聊天记录\"; //设置聊天记录保存路径byte[] buffer = new byte[5 * 1024 * 1024]; //创建接受消息缓存数组并约定缓存长度解决粘包问题const string connStr = @"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=D:\Code\WeTalk\WeTalk\WeTalkDataBase.mdf;Integrated Security=True"; //连接数据库标识Form lastForm; //上级窗体const int PORT = 8888; //设定端口号Socket socketClient = null; //创建全局变量客户端套接字public string strAccount; //创建全局变量用户名Thread thUpdateUser; //创建线程用于更新在线用户#endregionpublic FormTalk(string str,Form form){this.strAccount = str; //窗体间传值this.lastForm = form;InitializeComponent();}/* 更新在线用户线程函数 */void UpdateUser(){while (true) //死循环{/* 更新在线对象 */string sqlSelect = string.Format("select Account,IsOnline from [User]"); //SQL语句,选择表User中Account和IsOnline/// 创建对象时使用using可以在使用完该对象后,自动释放资源using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdSelect = new SqlCommand(sqlSelect, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接SqlDataReader sqlRead = cmdSelect.ExecuteReader(); //遍历数据库while (sqlRead.Read()){int index = lstAccount.FindString(sqlRead["Account"].ToString()); //列表中某值的下标// 列表里没有 但 在线的if (index == -1 && (int)sqlRead["IsOnline"] == 1){/* Action委托修改lstAccount控件内容 */this.Invoke(new Action(() =>{lstAccount.Items.Add(sqlRead["Account"].ToString());}));}// 列表里有 但 不在线的else if (index != -1 && (int)sqlRead["IsOnline"] == 0){/* Action委托修改lstAccount控件内容 */this.Invoke(new Action(() =>{lstAccount.Items.RemoveAt(index);}));}} conn.Close(); //关闭数据库连接}}}}/* 窗体打开时触发 */private void FormTalk_Load(object sender, EventArgs e){btnClean.Enabled = false;/* Action委托修改控件内容 */this.Invoke(new Action(() =>{lblAccount.Text = "用户名:" + this.strAccount; //显示用户名/* 权限设置 */if (strAccount == "admin")btnClean.Enabled = true;elsebtnClean.Enabled = false;}));/* 线程更新在线用户 */thUpdateUser = new Thread(UpdateUser); //实例化线程thUpdateUser.IsBackground = true;thUpdateUser.Start(); //启动线程try{socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //实例化TCP通信协议SocketIPEndPoint _port = new IPEndPoint(_ip, PORT); //创建连接对象socketClient.SendTimeout = 1000; //设置连接时限socketClient.BeginConnect(_port, new AsyncCallback(ConnectCallback), socketClient); //异步请求连接/// 参数1:服务器终结点,参数2:回调函数,参数3:传入回调函数的参数/* 写入历史聊天记录 */string log_path = _path + strAccount + "_log.txt";if (File.Exists(log_path)){string[] lines = File.ReadAllLines(log_path);foreach (string line in lines){this.Invoke(new Action(() =>{txtTalk.AppendText(line + "\r\n");}));}}}catch { } //异常捕获}/* 发送连接异步回调处理 */private void ConnectCallback(IAsyncResult ia){try{((Socket)ia.AsyncState).EndConnect(ia); //结束连接请求socketClient = ia.AsyncState as Socket; //将传入回调函数的参数实例化为Socket类byte[] sendBuffer = Encoding.Default.GetBytes(strAccount + ":上线了"); //缓存解码为字符串socketClient.Send(sendBuffer); //发送缓存至服务器socketClient.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), socketClient); //异步接受消息/// 参数1:接受消息缓存,参数2:接受消息缓存偏倚,参数3:接受消息缓存长度,参数4:套接字行为,参数5:回调函数,参数6:传入回调函数的参数}catch (Exception ex) //异常捕获{ShowMsg(ex.ToString());}} /* 显示消息函数 */private void ShowMsg(string str){string time = DateTime.Now.ToString("G"); //获取当前时间(长日期/时间)/* Action委托修改txtTalk控件内容 */this.Invoke(new Action(() =>{txtTalk.AppendText(time + "\r\n" + str + "\r\n");}));}/* 接受消息异步回调处理 */private void ReceiveCallback(IAsyncResult ia){socketClient = ia.AsyncState as Socket; //将传入回调函数的参数实例化为Socket类if (socketClient == null){return;}try{int bytesRead = socketClient.EndReceive(ia); //接受消息成功并返回消息长度 if (bytesRead > 0) //接受到非空消息{string context = Encoding.Default.GetString(buffer, 0, bytesRead); //缓存解码为字符串socketClient.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), socketClient); //异步接受消息/// 利用BeginReceive异步接受消息时会自动创建线程并自动结束线程if (!String.IsNullOrEmpty(context)){/* 线程锁——完成代码块后执行其他线程 */lock (socketClient){string[] strContext = context.Split(':'); //解码字符串if (strContext.Length > 0 && strContext[0] != strAccount)ShowMsg(context);/// 服务器转接的消息不是用户自生发出的数据时才显示}}}}catch(Exception ex) //异常捕获{ShowMsg(ex.ToString());}}/* 点击发送按键时触发 */private void btnSend_Click(object sender, EventArgs e){try{/* 发送消息至服务器 */byte[] buffer = Encoding.Default.GetBytes(strAccount + ":" + txtType.Text); //将泛型转换为数组socketClient.Send(buffer, 0, buffer.Length, SocketFlags.None); //客户端发送消息至服务器/// 参数1:接受消息缓存,参数2:接受消息缓存偏倚,参数3:接受消息缓存长度,参数4:套接字行为/* Action委托修改控件内容 */this.Invoke(new Action(() =>{ShowMsg(strAccount + ":" + txtType.Text);txtType.Text = "";}));}catch(Exception ex){ShowMsg(ex.ToString());}}/* 点击重置按键时触发 */private void btnClear_Click(object sender, EventArgs e){/* 清空数据库 */string sqlDelete = "delete from [User]"; //SQL语句,清空User内容using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdDelete = new SqlCommand(sqlDelete, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接//执行非查询命令时使用ExecuteNonQuery,会返回影响的行数cmdDelete.ExecuteNonQuery(); //执行SQL语句conn.Close(); //关闭数据库连接ShowMsg("管理员重置了所有账号");}}}/* 关闭窗体后触发 */private void FormTalk_FormClosed(object sender, FormClosedEventArgs e){try{string sqlUpdate = string.Format("update [User] set IsOnline={0} where Account='{1}'", 0, strAccount); //SQL语句,更新isOnline为上线状态using (SqlConnection conn = new SqlConnection(connStr)) //创建数据库连接类{using (SqlCommand cmdUpdate = new SqlCommand(sqlUpdate, conn)) //创建数据库命令类{conn.Open(); //打开数据库连接//执行非查询命令时使用ExecuteNonQuery,会返回影响的行数cmdUpdate.ExecuteNonQuery(); //执行SQL语句conn.Close(); //关闭数据库连接System.Environment.Exit(0); //杀死进程}}}catch { } //异常捕获}/* 关闭窗体时触发 */private void FormTalk_FormClosing(object sender, FormClosingEventArgs e){DialogResult res = MessageBox.Show("是否保存聊天记录?", "提示", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); //保存结果信息/// 参数1:显示文本,参数2:标题,参数3:按键类型,参数4:显示图标if (res == DialogResult.Yes){if (!System.IO.Directory.Exists(_path))System.IO.Directory.CreateDirectory(_path);/* 保存聊天记录 */string log_path = _path + strAccount + "_log.txt";using (FileStream logFileStream = new FileStream(log_path, FileMode.Create, FileAccess.Write)) //创建写入文件{using (StreamWriter logWriter = new StreamWriter(logFileStream)){logWriter.Write(txtTalk.Text);logWriter.Close();}logFileStream.Close();}e.Cancel = false; //确认关闭窗体}else if (res == DialogResult.No) //不保存聊天记录{e.Cancel = false; //确认关闭窗体}else //取消{e.Cancel = true; //取消关闭窗体}}/* 点击右键菜单项时触发 */private void contextMenuStrip_ItemClicked(object sender, ToolStripItemClickedEventArgs e){contextMenuStrip.Items[0].Click += new EventHandler(clearTxtTalk); //定义EvendHandler委托}/* 聊天区记录清空函数 */private void clearTxtTalk(object sender, EventArgs e){this.Invoke(new Action(() =>{txtTalk.Text = "";}));}}
}
结尾与展示
WeTalk聊天室后续可以升级成可以一对一私聊的聊天室,此时需要共同约定一个消息的格式也被称为协议,在协议中隐含了特定的用来交互的信息,例如本文的聊天室所设计的消息格式为用户名:消息内容,利用:将用户名与消息内容分割开,升级成可一对一私聊的聊天室后可以把消息格式设计为接受者:发送者:消息,在服务器中把用户名与套接字用字典进行存储并查询即可实现能够一对一私聊的聊天室,读者可自行尝试升级。
更多推荐
.NET编程——利用C#实现基于Socket类的聊天室(WinForm)
发布评论