计算机学院课程设计
专 业: 08网络工程
课程名称: TCP/IP协议分析实验
课题名称: 基于TCP协议的C#聊天程序
教 师: 桂学勤
学 号: 084221019
时 间: 2010 年 6月 18日
目录
一:需求分析 2
1.1编写目的 2
1.2开发环境 2
1.3功能介绍 2
1.4Socket通信机制 2
二:代码实现 3
2.1服务器端主要代码 3
2.2客户端主要代码 10
三:程序运行界面 14
3.1登录界面 14
3.2登录成功后的聊天界面 14
3.3服务器端显示界面 15
四:总结 15
基于TCP协议的C#聊天程序
前言
随着互联网技术的飞速发展,基于网络的即时通信技术也给人们带来了诸多便利,人们也慢慢体会到了网上聊天的乐趣与无拘束的感觉。聊天工具作为当今使用最为广泛的即时通信工具之一,可以方便的同网络上的好友在线交流。在中国,最流行的莫过于腾讯公司的QQ,伴随着技术的不断升级,腾讯公司也为我们带来了越来越多的精彩的服务。这里我将利用Socket编程技术模拟QQ聊天功能,实现一个简单的室。
一:需求分析
1.1编写目的
编写该软件能够对自己所学的东西进行一次系统的回顾,加深对TCP协议的理解以及提升自己实际开发的能力。
1.2开发环境
操作系统:windows xp sp3
内存:2G
CPU:AMD Athlon(tm) 64 X2 Dual Core Processor 5200+ 2.71GHz
编程软件:Microsoft Visual Studio 2010
1.3功能介绍
该程序是利用c#语言编写的一个基于Socket的简单聊天软件,最要实现了用户登录,但登录时只需要提供用户名,不需要输入密码。具有私聊和聊两种聊天模式,即允许多人,并且在线用户聊天时,可以将消息发送给一个用户,亦可以将消息发送给所有人。聊天的消息内容包括:用户名称、发送时间、发送正文、以及消息模式。断开连接的同时会关闭客户端,此时用户若希望聊天,需要再次登录服务器。
1.4Socket通信机制
Socket编程是建立在应用层TCP/IP协议之上的。目前最流行的是客户机/服务器模式,在
面向连接的 Client/Server 模型中,Server 端的 socket 总是等待一个 Client 端的请求。客户机/服务器模型的工作流程图如下图所示:
服务器程序特点:
1 一般启动后就一直处于运行状态,以等待客户机进程的请求;
2 使用的端口往往是熟知端口,便于客户机进程连接请求;
3 一般拥有较多的系统资源,以便及时响应各个客户机进程的请求;
4 可以并行处理多个客户机进程的请求,但数目是有一定的限制;
5 在通信时一般处于被动的一方,不需要知道客户机的IP地址和端口信息。
客户机程序的特点:
① 在需要服务器进程的服务时将向服务器进程请求服务,并建立通信连接,得到满足并完成处理后就终止通信连接;
② 使用向系统申请的临时端口与服务器进程进行通信,通信完成后将释放该端口;
③ 拥有相对较少的系统资源;
④ 在通信时属于主动的一方,需要事先知道服务器的IP地址和端口信息
客户机与服务器模式又分为两大类:面向连接的交互(TCP)和面向无连接的交互(UDP),本程序是面向连接的交互,交互机制如下图:
二:代码实现
2.1服务器端主要代码
负责监听客户端请求,并根据客户端命令执行不同操作的Listener类,Listener.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net.Sockets; using System.Net; using System.Threading; using System.Collections.Specialized; using System.Runtime.Serialization; using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace TalkerServer { class Listener { public delegate void ShowMsg(ListenWindow lisWin, string svrInfo);//svrInfo要显示的服务器信息,lisWin显示的窗口对象 private ListenWindow lisWin = null;//服务器监听窗口对象 private ShowMsg smsg = null;//消息显示委托对象 private int listenPort = 8888;//监听端口,默认8888 private const int maxPacket = 64 * 1024;//缓冲区大小 private Dictionary<string,Socket> userMap = new Dictionary<string,Socket>();//所有登陆服务器的用户map private TcpListener tcpListener = null;//侦听器 public int ListenPort { get { return this.listenPort;} set { this.listenPort = value; } } public TcpListener TcpListener { get { pListener; } set { pListener = value; } } public Dictionary<string, Socket> UserMap { get { return this.userMap; } set { this.userMap = value; } } public void Listen(ShowMsg smsg,string svrInfo,ListenWindow lisWin) { this.lisWin = lisWin; this.smsg = smsg; string connInfo = string.Empty; IPAddress ipAddr = Dns.GetHostAddresses(Dns.GetHostName())[0]; tcpListener = new TcpListener(ipAddr,listenPort); tcpListener.Start(); connInfo += "服务器已经启动,正在监听客户端的连接……\r\n"; smsg(lisWin, connInfo); while (true) { byte[] packetBuff = new byte[maxPacket]; Socket client = tcpListener.AcceptSocket(); client.Receive(packetBuff); string userName = Encoding.Unicode.GetString(packetBuff).TrimEnd('\0'); if (userMap.ContainsKey(userName) && userMap.Count != 0) { client.Send(Encoding.Unicode.GetBytes("0"));//该用户存在 } else { client.Send(Encoding.Unicode.GetBytes("1"));//该用户不存在 } userMap.Add(userName,client);//保存客户端到Map中 string svrlog = string.Format("[系统消息]新用户【{0}】在【{1}】已连接... 当前在线人数:【{2}】\r\n\r\n", userName, DateTime.Now, userMap.Count); smsg(lisWin, svrlog);//利用委托更新服务器显示日志 Thread clientThread = new Thread(new ParameterizedThreadStart(ThreadFunc));//开启一个子线程执行程序 clientThread.Start(userName); foreach(KeyValuePair<string,Socket> user in userMap) { string uName = user.Key as string; Socket clientSkt = user.Value as Socket; if (!uName.Equals(userName)) { clientSkt.Send(Encoding.Unicode.GetBytes(svrlog)); } } } } //序列化在线用户列表 private byte[] SerializeOnlineUserList(Object obj) { StringCollection onlineUserList = new StringCollection(); foreach (object o in userMap.Keys) { if (o != obj)//序列化的在线列表中不包含自身登陆用户 { onlineUserList.Add(o as string);//转换语句 } } IFormatter format = new BinaryFormatter();//以二进制格式将对象或整个连接对 MemoryStream stream = new MemoryStream(); format.Serialize(stream, onlineUserList);//保持到内存流 byte[] ret = stream.ToArray(); stream.Close(); return ret; } public void ThreadFunc(Object obj) { Socket client = null; string userName = (string)obj; if (userMap.TryGetValue(userName, out client)) { if (client != null) { while (true) { try { byte[] _cmdBuff = new byte[128]; client.Receive(_cmdBuff);//第一次接收命令,第二次接收内容 string _cmd = string.Concat(_cmdBuff[0].ToString(), _cmdBuff[1].ToString()); //00对所有人,01请求用户列表,02断开连接 if (_cmd == "00") { byte[] _msgBuff = new byte[maxPacket]; client.Receive(_msgBuff); foreach (KeyValuePair<string, Socket> user in userMap) { string uName = user.Key as string; Socket clientSkt = user.Value as Socket; if (!uName.Equals(userName)) { clientSkt.Send(_msgBuff); } } } else if (_cmd == "01")//请求用户列表 { byte[] onlineBuff = SerializeOnlineUserList(obj); //先发送响应信号,用于客户机的判断,"11"表示服务发给客户机的更新在线列表的命令 client.Send(new byte[] { 1, 1 }); client.Send(onlineBuff); } else if (_cmd == "02")//与服务器断开连接 { userMap.Remove(userName); string svrlog = string.Format("[系统消息]用户【{0}】在【{1}】已断开... 当前在线人数:【{2}】\r\n\r\n", userName, DateTime.Now, userMap.Count); foreach (KeyValuePair<string, Socket> user in userMap) { string uName = user.Key as string; Socket clientSkt = user.Value as Socket; clientSkt.Send(Encoding.Unicode.GetBytes(svrlog));//给其他用户通知现在上线的用户 } smsg(lisWin, svrlog);//利用委托更新服务器显示日志 Thread.CurrentThread.Abort();//终止为该客户端开启的线程 } else//发送给指定用户 { string _receiver = Encoding.Unicode.GetString(_cmdBuff).TrimEnd('\0'); byte[] _packetBuff = new byte[maxPacket]; client.Receive(_packetBuff); if (userMap.ContainsKey(_receiver))//是否存在该接收者 { //通过转发表查接收方的套接字 Socket receiverSkt = userMap[_receiver] as Socket; receiverSkt.Send(_packetBuff); } else { string sysMessage = string.Format("[系统消息]您刚才的内容没有发送成功。\r\n可能原因:用户【{0}】已离线或者网络阻塞。\r\n\r\n", _receiver); client.Send(Encoding.Unicode.GetBytes(sysMessage)); } } } catch (SocketException) { string svrlog = string.Format("[系统消息]用户【{0}】的客户端在【{1}】意外终止!当前在线人数【{2}】\r\n\r\n", userName, DateTime.Now, userMap.Count); smsg(lisWin, svrlog);//利用委托更新服务器显示日志 //向所有客户机发送系统消息 foreach (KeyValuePair<string, Socket> user in userMap) { string uName = user.Key as string; Socket clientSkt = user.Value as Socket; clientSkt.Send(Encoding.Unicode.GetBytes(svrlog)); } Thread.CurrentThread.Abort(); } } } } } } } |
负责显示客户端连接信息的服务器窗口的后台处理代码:
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; namespace TalkerServer { public partial class ListenWindow : Form { private Listener listener = null; private Thread listenThread = null; delegate void SetTextCallback(ListenWindow lisWin,string text); public ListenWindow() { InitializeComponent(); listener = new Listener(); } private void btnListen_Click(object sender, EventArgs e) { this.btnListen.Enabled = false; int port = this.listener.ListenPort ; if (int.TryParse(txtEndpoint.Text, out port)) { if (port < 1024 || port > 65535) { MessageBox.Show("端口号不合法!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } this.listenThread = new Thread(new ThreadStart(this.ThreadProcSafe)); this.listenThread.Start(); } else { MessageBox.Show("端口号不合法!","提示",MessageBoxButtons.OK,MessageBoxIcon.Information); return; } } public void showSvgMsg(ListenWindow lisWin ,string svrInfo) { ConnInfo.Text = svrInfo; } #region /// <summary> /// 1:访问 Windows 窗体控件本质上不是线程安全的。 /// 2:如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。 /// 3:如果从创建控件的线程之外的其他线程试图更改控件的状态,一半情况下是不行的 /// 4:以事件句柄创建一个以线程安全方式调用windows窗体控件的线程。 /// </summary> public void ThreadProcSafe()//线程安全访问控件 { this.listener.Listen(SetText,"",this); } private void SetText(ListenWindow lisWin,string text) { if (ConnInfo.InvokeRequired) { SetTextCallback d = new SetTextCallback(SetText); lisWin.Invoke(d, new object[] { this,text }); } else { ConnInfo.AppendText(text); } } #endregion private void btnStopListen_Click(object sender, EventArgs e) { if (listener != null) { if (listener.TcpListener != null) { listener.TcpListener.Stop(); } if (listener.UserMap.Count != 0) { foreach (Socket socket in listener.UserMap.Values) { socket.Shutdown(SocketShutdown.Both); } listener.UserMap.Clear(); listener.UserMap = null; } listener = null; this.btnListen.Enabled = true; } } } } |
2.2客户端主要代码
客户端聊天窗口后台代码:
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.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.IO; using System.Collections.Specialized; namespace TalkClient { public partial class TalkWindow : Form { private string _username = null;//登陆用户名 private int maxPacket = 2048;//2K的缓冲区 private Thread receiveThread = null;//用于接收消息 private NetworkStream _nws = null;//网络数据流 delegate void SetTextCallback(TalkWindow talkWin, string text); /// <summary> /// 处理接收到的消息 /// </summary> private void MsgHandle() { while (true)//无限循环,不断接收消息 { byte[] packet = new byte[maxPacket]; _nws.Read(packet, 0, packet.Length); string msg = string.Concat(packet[0].ToString(), packet[1].ToString()); if (msg == "11")//更新在线用户列表 { byte[] onlineBuff = new byte[maxPacket]; int byteCnt = _nws.Read(onlineBuff, 0, onlineBuff.Length); IFormatter format = new BinaryFormatter(); MemoryStream stream = new MemoryStream(); unicode在线工具 stream.Write(onlineBuff, 0, byteCnt); stream.Position = 0; StringCollection onlineList = (StringCollection)format.Deserialize(stream); drpReceiver.Items.Clear(); foreach (string onliner in onlineList) { if (!onliner.Equals(_username)) { drpReceiver.Items.Add(onliner); } } } else { string displaytxt = Encoding.Unicode.GetString(packet); SetText(this,displaytxt);//添加到聊天内容中 } } } /// <summary> /// 从非创建该控件的线程访问该控件,并且修改其Text属性 /// </summary> /// <param name="talkWin"></param> /// <param name="text"></param> public void SetText(TalkWindow talkWin, string text) { if (AllMsg.InvokeRequired) { SetTextCallback stc = new SetTextCallback(SetText); talkWin.Invoke(stc, new object[] { this,text }); } else { AllMsg.AppendText(text); } } private void btnSend_Click(object sender, EventArgs e) { string localTxt = "";//本地显示消息 string remoteTxt = "";//其它用户显示的消息 string msg = txtSelfMsg.Text.Trim();//本人将要发送的消息 if (msg == string.Empty) { MessageBox.Show("不能发送空消息","提示",MessageBoxButtons.OK,MessageBoxIcon.Information); return; } //判断选择的聊天模式 if (drpTalkModem.SelectedIndex == 0)//悄悄话模式 { string receiver = drpReceiver.Text; if (receiver == string.Empty) { MessageBox.Show("请选择发送悄悄话的人,若列表中为空,表明当前只有你一人在线!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } else { localTxt = string.Format("[私聊]您在 {0} 对 {1} 说:\r\n{2}\r\n\r\n", DateTime.Now, receiver, msg); remoteTxt = string.Format("[私聊]{0} 在 {1} 对您说:\r\n{2}\r\n\r\n", _username, DateTime.Now, msg); _nws.Write(Encoding.Unicode.GetBytes(receiver), 0, Encoding.Unicode.GetBytes(receiver).Length); } } else { localTxt = string.Format("[聊]您在 {0} 对所有人说:\r\n{1}\r\n\r\n", DateTime.Now, msg); remoteTxt = string.Format("[聊]{0} 在 {1} 对所有人说:\r\n{2}\r\n\r\n", _username, DateTime.Now, msg); _nws.Write(new byte[] { 0, 0 }, 0, 2); } _nws.Write(Encoding.Unicode.GetBytes(remoteTxt), 0, Encoding.Unicode.GetBytes(remoteTxt).Length); txtAllMsg.AppendText(localTxt);//添加发送的消息到聊天记录里 txtSelfMsg.Clear();//清空刚编辑发送的消息 } /// <summary> /// 聊天模式,选择悄悄话,左边的在线用户列表可用,可以选择里面的任意用户进行聊天 /// 选择所有人,消息将会发给所有人 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void drpTalkModem_SelectedIndexChanged(object sender, EventArgs e) { if (drpTalkModem.SelectedIndex == 0) { drpReceiver.Enabled = true; } else { drpReceiver.Enabled = false; } } /// <summary> /// 当用户点击下拉列表时触发事件,请求服务器更新在线用户列表 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void drpReceiver_SelectedIndexChanged(object sender, EventArgs e) { _nws.Write(new byte[] { 0, 1 }, 0, 2); } private void btnClose_Click(object sender, EventArgs e) { DialogResult ret; ret = MessageBox.Show("确定与服务器断开连接吗?","退出",MessageBoxButtons.OKCancel, MessageBoxIcon.Question,MessageBoxDefaultButton.Button2); if (ret == DialogResult.OK) { _nws.Write(new byte[] { 0, 2 }, 0, 2); if (receiveThread != null) { receiveThread.Abort(); } _nws.Close(); this.Owner.Close(); this.Close(); } } private void TalkWindow_Load(object sender, EventArgs e) { receiveThread = new Thread(new ThreadStart(MsgHandle)); receiveThread.Start(); } } } |
三:程序运行界面
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论