在上一节中已经给大家讲述了即时通信程序的通信流程,以及相应的通信格式,在这一节中我会带领大家搭建即时通信程序的服务器端。
在这一节中我们用到的知识有TCPListener、套接字(Socket)多线程(Thread)、文件流(FileStream)、、Dictionary<T,T>集合。
首先新建一个WinForm应用程序,页面布局如下:
页面布局:两个单行文本框分别为服务器监听的IP(txtIP)和端口(txtPort),
一个多行文本框(txtServer)来显示服务器运行情况,
一个ListBox(onLineListServer)用来显示在线用户
两个按钮分别负责启动服务(btnqd)和退出(btnexit)
首先需要创建一些全局变量如下:
#region Members
private const int MAX=100 //设置最大的监听队列,超过该队列之后服务器不再接收请求 //创建全局的TCPListener的实例,专门负责客户端的连接请求 private TcpListener tcpListener; //临时套接字,临时存储由tcpListener创建的负责通信的套接字 private Socket lsSocket; //用来存储负责和客户端通信的套接字键为客户端登录名,值为负责与客户端通信的套接字,所有的键即为在线用户,和onLineListServer的Item一一对应 public Dictionary< string , Socket> socketDictionary = new Dictionary< string , Socket>(); |
接下来就是需要在构造函数里做一些处理了,在构造函数里,我们主要是加载一个皮肤文件,同时获取本机的IP,指定监听端口,这里要注意尽量把指定端口设置大一点,防止和特定端口进行冲突。
代码如下:
public ChatServer() { InitializeComponent(); skinEngine1.SkinFile = "SteelBlack.ssk" ; //加载皮肤文件 txtServerIP.Text = Dns.GetHostByName(Dns.GetHostName()).AddressList[0].ToString(); //动态获取本机IP txtServerPort.Text = "11615" ; TextBox.CheckForIllegalCrossThreadCalls = false ; //取消TextBox的跨线程操作检验,即允许在非UI线程中访问TextBox控件 ListBox.CheckForIllegalCrossThreadCalls = false ; //和上面TextBox效果一样 } |
以上这些准备工作做好之后,我们就可以启动服务了,所以在启动服务按钮的Click事件里我们需要将tcpListener给实例化,同时要打开监听服务,开始监听客户端的请求。这里就遇到了一个问题,因为我们不知道客户端什么时候请求,所以这个时候我们就需要新建一个线程该线程专门负责监听客户端的请求,我们称之为监听线程。
代码如下:
private void btnqd_Click( object sender, EventArgs e) { tcpListener= new TcpListener(IPAddress.Parse(txtServerIP.Text),Convert.ToInt32(txtServerPort.Text)); tcpListener.Start(); Thread listenThread= new Thread( new ThreadStart(Listening)); listenThread.Start(); listenThread.IsBackground= true ; txtServer.Text += "服务器已启动!\r\n" ; } |
在我们创建的监听线程里执行了Listening函数,在Listening函数里我们会用tcpListener创建一个专门负责监听的套接字,并将该套接字存储在临时变量lsScoket中,这个时候我们会再创建一个线程,专门来负责跟客户端的通信,我们称之为通信线程。每监听到一个客户端的请求,我们都会创建一个相应的通信套接字,和一个专门负责通信的通信线程。
代码如下:
private void Listening() { while ( true ) { Socket comminuteSocket = tcpListener.AcceptSocket(); this .lsSocket = comminuteSocket; if (socketDictionary.Count < 100) //如果客户端请求数量大于100,则将该请求的连接关闭,即服务端拒绝为其服务 { Thread serviceThread = new Thread( new ThreadStart(Service)); serviceThread.IsBackground = true ; serviceThread.Start(); } else { this .lsSocket.Close(); } } |
在通信线程里,我们才真正的涉及到跟客户端的通信,会将客户端发送的消息进行解析和处理,我们在上一节中定制了特定的通信格式,在这里我再重申一下。
1、首先对接收过来的字节数组进行判断,如果接收过来的字节数组的首字节buffer[0]==0
则说明客户端发送过来的是文件,这个时候我们只需要遍历在线人员名单,也就是遍历socketDictionary的键,并且通过与键对应的Socket类型的通信套接字将该文件字节数组转发给每一个在线的客户端。
2、接受过来的字节数组的首字节buffer[0]!=0,即说明客户端发送过来的是消息字符串,这个时候我们就要对该消息字符串做进一步的解析:
2.1、"On|上线者名称|":如果接受的消息是这样的格式的话,则服务器需要对该字符串进行分割,取上线者名称,并对其进行判断,如何socketDictionary字典中没有这个键值,则将其加入onLineListServer,将其对应的通信套接字加入socketDictionary字典中,如果有这样的键存在,则自动在将键增加一个字符“1”,并向所有的在线人员发送通知,格式内容如下:"OnLine|'上线者名称'上线啦!|",如果要通知所有已在线的人员更新在线人员名单,格式内容如下:"Off|name1、name2、name3、...|
2.2、"MSGALL|发送者名字|发送内容|",服务器遍历在线人员名单并向所有在线人员发送信息"'发送者名字'说:'发送内容'"
2.3、"MSG|接受者名字|发送者名字|发送内容|",服务器先从socketDictionary字典中找到与接受者名字一样的key所对应的通信套接字,并利用该通信套接字向客户端发送信息,格式内容如下:"'发送者名字'说:'发送内容'"
2.4、"Off|发送者名字|",服务器首先从socketDictionary字典中找到与接受者名字一样的key所对应的通信套接字,将其从中删除,并且将其名字从在线列表中移除,并向所有在线人员发送新的在线人员名单,进行在线人员更新,格式内容如下:"Off|nam1、name2、name3......|"
代码如下:
private void Service() { byte [] buffer = new byte [1024 * 1024 * 10]; Socket commSocket = this .lsSocket; bool connected = true ; while (connected) { //接受传输过来的字节流 int length = commSocket.Receive(buffer); if (buffer[0] == 0) //如果缓冲区字节数组的首字节值为0,则为文件,直接保存 { for ( int i = 0; i < onlineListServer.Items.Count; i++) { socketDictionary[onlineListServer.Items[i].ToString()].Send(buffer); } } else { string Msg = Encoding.Default.GetString(buffer); string [] msg = Msg.Split( new char [] { '|' }); //当有新的成员上线的时候,在登录连接时向服务器发送一个信息,告诉服务器****上线了,然后服务器会像所有在线人员发送信息:<br> //“*****上线了”,并且发送新的在线人员名单,让所有在线人员更新各自的在线人员名单。 if (msg[0] == "ON" ) { string online= null ; if (!socketDictionary.ContainsKey(msg[1])) //如果Socket字典中不存在该键,则立刻添加 { online= msg[1] + "上线啦!\r\n" ; commSocket.Send(Encoding.Default.GetBytes( "0" )); socketDictionary.Add(msg[1], lsSocket); onlineListServer.Items.Add(msg[1]); txtServer.Text += msg[1]; } else { commSocket.Send(Encoding.Default.GetBytes( "1" )); //如果该Socket字典中存在该键,则将该键增加一个字符‘1’之后再添加,防止重名冲突 socketDictionary.Add(msg[1]+ "1" ,lsSocket); onlineListServer.Items.Add(msg[1]+ "1" ); txtServer.Text+=msg[1]+ "1" ; online=msg[1] + "1上线啦!\r\n" ; } txtServer.Text += "上线啦!\r\n" ; string names = "" ; for ( int i = 0; i < onlineListServer.Items.Count; i++) { names += onlineListServer.Items[i].ToString() + "、" ; } names += "|" ; //向所有在线人员发送“***上线了”消息,并且发送最新在线人员名单,通知其更新在线人员信息。 for ( int i = 0; i < onlineListServer.Items.Count; i++) { socketDictionary[onlineListServer.Items[i].ToString()].Send(Encoding.Default.GetBytes( "OnLine|" + online)); Thread.Sleep(100); socketDictionary[onlineListServer.Items[i].ToString()].Send(Encoding.Default.GetBytes( "Off|" + names)); } } //向所有在线人员群发信息 else if (msg[0] == "MSGALL" ) { foreach ( string receiever in socketDictionary.Keys) { socketDictionary[receiever].Send(Encoding.Default.GetBytes(msg[1] + "说:" + msg[2])); } } //向指定的某个人发送信息 else if (msg[0] == "MSG" ) { socketDictionary[msg[1]].Send(Encoding.Default.GetBytes(msg[2] + "说:" + msg[3])); } else if (msg[0] == "Off" ) { string names = "" ; socketDictionary.Remove(msg[1]); onlineListServer.Items.Remove(msg[1]); for ( int i = 0; i < onlineListServer.Items.Count; i++) { names += onlineListServer.Items[i].ToString() + "、" ; } names += "|" ; for ( int i = 0; i < onlineListServer.Items.Count; i++) { socketDictionary[onlineListServer.Items[i].ToString()].Send(Encoding.Default.GetBytes( "Off|" + names)); } } } } } |
这一节到这里,不知不觉间我们已经把即时通信程序的服务端给搭建好了。在这一节里你需要明白的是服务器端的工作流程,即我们常说的"三次握手",第一次客户端向服务器端发请求建立连接,第二次服务端创建专门负责通信的套接字,第三次专门负责通信的套接字跟客户端进行通信。同时我们还要理解掌握它是如何利用多线程进行客户端监听、通信的。在这里我们自己定义了一些通信格式,这些通信格式就跟HTTP协议一样,只不过我们定义的格式仅在我们小范围内适用。
好了这一节就到这里了,希望可以对大家有所帮助,也还请大家多多指点。在下一节中我会带领大家把客户端给实现。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构