利用TCP传输协议实现基于Socket的聊天程序(高级版_多线程)

大家好,在上篇利用TCP和UDP协议,实现基于Socket的小聊天程序(初级版)》博客中,所写程序只是实现简单的连接通信,基于控制台实现,运用了TCP和UDP两种传输协议。今天我和大家分享一个基于窗体的聊天程序,使用了多线程,实现的功能类似于QQ的聊天,不同的是只有一个服务器端,但可以有多个客户端与其通信,只能实现简单的文字信息交流。。。

同样,这个聊天程序也需要一个服务器端,和N个客户端来模拟实现,首先我们来搭建服务器端

首先贴上服务器端的界面图:

界面很简单,左边一个客户端在线的列表,一个显示消息的文本框和一个发送消息的文本框,为了演示简单,我把IPPort都固定为127.0.0.18888

首先我们来看看【启动服务器按钮的代码:

启动服务器
 1       //负责监听 客户端 连接的线程
 2         Thread threadWatch = null;
 3         
 4         //负责监听的套接字
 5         Socket socketServer = null;
 6 
 7         private void btn_StartServer_Click(object sender, EventArgs e)
 8         {
 9             //创建 服务器 负责监听的套接字 参数(使用IP4寻址协议,使用流式连接,使用TCP传输协议)
10             socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
11             
12             //获取IP地址
13             IPAddress ip = IPAddress.Parse(tb_IP.Text.Trim());
14             
15             //创建 包含IP和Port的网络节点对象
16             IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(tb_Port.Text.Trim()));
17 
18             //将负责监听 的套接字 绑定到 唯一的IP和端口上
19             socketServer.Bind(endPoint);
20 
21             //设置监听队列 一次可以处理的最大数量
22             socketServer.Listen(10);
23 
24             //创建线程 负责监听
25             threadWatch = new Thread(WatchConnection);
26             //设置为后台线程
27             threadWatch.IsBackground = true;
28             //开启线程
29             threadWatch.Start();
30 
31             ShowMsg("=====================服 务 器 启 动 成 功======================");
32             
33         }

这里的代码其实和初级版聊天程序中的服务器代码并没有多大差别,只是在这里我们使用了线程来专门负责监听客户端的请求,但是我们这里为什么要使用多线程呢??这就需要对多线程的概念及作用去做一个了解了,这里我简单的说下,我们的程序运行是由一个主线程在执行着,而Socket的Accept()方法执行的时候会阻断线程,如果我们没有使用多线程的话也就是说我们的主线程被阻断了,这是就会出现一个现象就是程序卡死来那里了,只有等到某个客户端连接到该服务器,Accept()方法接受到了客户端的请求了主线程才会被释放出来,为了避免这种情况,所以我们这里用过了另外一个线程专门来负责监听客户端的请求,使得主线程仍然可以自由执行(个人理解)。线程实例化时要求传入参数,参数的本质就是一个委托,所以在实例化的时候要求传入的是一个方法作为参数,所以我们传入WatchConnection负责监听的方法,贴上该方法的代码,如下:

WatchConnection方法
       //用来存储返回的新的用于通信的套接字 
        Dictionary<string, Socket> socketDic = new Dictionary<string, Socket>();
        //用来接收数据的线程
        Thread threadRec = null;
        //监听方法
        void WatchConnection()
        {
            //持续不断的监听
            while (true)
            {   
                
                //开始监听 客户端 连接请求 【注意】Accept方法会阻断当前的线程--未接受到请求 程序卡在那里
                Socket sokConnection = socketServer.Accept();//返回一个 负责和该客户端通信的 套接字
                
//将返回的新的套接字 存储到 字典序列中
                socketDic.Add(sokConnection.RemoteEndPoint.ToString(), sokConnection);
                //向在线列表中 添加一个 客户端的ip端口字符串 作为客户端的唯一标识
                lb_OnlineList.Items.Add(sokConnection.RemoteEndPoint.ToString());
                //打印输出
                ShowMsg("客户端连接成功:"+sokConnection.RemoteEndPoint.ToString());

                //为该通信Socket 创建一个线程 用来监听接收数据
                threadRec = new Thread(RecMsg);
                threadRec.IsBackground = true;
                threadRec.Start(sokConnection);

            }
        }

其实这个方法里的内容很多,首先就我们要实现的效果,我们来思考一个问题,我们要实现的效果是一个服务器可以和多个客户端进行交流,那服务器怎么知道是哪个客户端发过来的呢?Socket的Accept()方法都会返回一个新的Socket对象,那当我们在不同的客户端向服务器端发送数据时我们该用哪个返回的Socket对象呢??说说我的想法吧。。我的想法是把每个返回的Socket对象都存放起来,用的时候再对应的去拿就可以了,这时我就想到了字典(Dictionary)集合,好,一个问题解决了,但还有一个问题,就是我们根据什么去拿对应的Socket对象呢??字典就是一个键值对,我们已经解决了这个值,我们该用干什么键去拿呢??所谓键,就是要唯一能标识的东西,所以这是我们就想到了IP和Port了,这个能唯一标识是那个客户端,所以我们可以通过返回的新的Socket对象的RemoteEndPoint属性来拿到客户端的IP和Port信息,这样,我们两个问题都解决了,这样我们就存储了返回的Socket对象及对应客户端的Ip和Port,然后我们在服务器的“在线列表”上添加上该客户端的IP和Port,用于服务器向客户端发送信息时选择对应的客户端。还有就是,很明显的能看到一个while的死循环,为什么要用死循环呢?因为如果没有用循环,当客户端只有一个的情况下,来连接到服务器,没问题,服务器可以接受到请求,但如果多个呢,,这时就出问题了,之后连接的客户端的请求都接受不到了,所以我们要用一个死循环,来持续的监听请求。最后就是会发现,在这个方法中我又用到了一个线程,这个线程是干嘛的呢?其实,大家可以想想,当客户端给服务器发送信息时,服务器端需要来监听信息,也就是消息过来时要执行Socket的Receive方法来接受消息,当然我们要求务器端要持续的监听接收客户端发来的消息,所以我们就想到线程了,在服务器端用一个线程专门来接收客户端发来的信息,所以我就将返回的Socket对象作为线程的参数传递给对应的方法,线程的参数传递是在Start()方法中实现。

下面贴上此方法中使用了线程的RecMsg方法:

RecMsg方法
 1       //接受数据的方法
 2         void RecMsg(object socket)
 3         {
 4             //持续监听接收数据
 5             while (true)
 6             {
 7                 //实例化一个字符数组
 8                 byte[] data = new byte[1024 * 1024];
 9                 //接受消息数据
10                 int receiveBytes = ((Socket)socket).Receive(data);
11                 //转换成字符串
12                 string recMsg = Encoding.UTF8.GetString(data, 0, receiveBytes);
13                 //打印接收到的数据
14                 ShowMsg(((Socket)socket).RemoteEndPoint.ToString() + ":" + recMsg);
15             }
16         }

 可以看到该方法的参数来下是object,这是由于线程传参就是要求参数的类型要是object型的,我们仍然用了一个while死循环来实现持续监听接受数据,至于接受的其他代码就和初级版中的一样了,不多做介绍了,但这里需要注意一点,因为我们在线程的方法中对主线程的控件进行了操作,所以直接运行会报错,解决方法,我们在窗体的构造函数中添加代码,使其不对TextBox进行跨线程检测,代码如下:

窗体构造函数
1         public FormClient()
2         {
3             InitializeComponent();
4 
5             //关闭对文本框的 跨线程操作
6             TextBox.CheckForIllegalCrossThreadCalls = false;
7         }

前面两个方法中都用到了ShowMsg()这个方法了,至于这个方法,很简单了,作用就是在“显示消息”文本框中追加消息而已,为了代码重用就写了个方法,贴上代码:

ShowMsg方法
1         //打印输出
2         void ShowMsg(string msg)
3         {
4             tb_MsgShow.AppendText(msg + "\r\n");
5             
6         }

然后我们来看下【发送按钮】的代码:

发送按钮事件
       //发送消息到客户端
        private void btn_SendMsg_Click(object sender, EventArgs e)
        {
            //调用发送方法
            Send();
        }

Send()方法代码:

Send方法
 1        /// <summary>
 2        /// 发送消息到客户端
 3         /// </summary>
 4         void Send()
 5         {
 6             if (lb_OnlineList.Text == "")
 7             {
 8                 MessageBox.Show("请先选择一个客户端");
 9                 return;
10             }
11             //获取发送信息
12             string Message = tb_InputMsg.Text.Trim();
13             //将字符串转换成字节数组
14             byte[] data = System.Text.Encoding.UTF8.GetBytes(Message);
15             //找到对应的客户端 并发送数据
16             socketDic[lb_OnlineList.Text].Send(data, SocketFlags.None);
17             //打印输出
18             ShowMsg("发送数据:" + Message);
19             //清空输入消息的内容
20             tb_InputMsg.Text = "";
21 
22         }

 

这样我们服务器端就搭建完成了,接下来看看怎么搭建客户端了,,

首先贴上客户端界面的效果:

 界面也非常的简单,就是一个【连接服务器】按钮、一个【发送】按钮,一个“显示消息” 框和一个“输入消息” 框。。。

 

同样我们来看看【连接服务器】按钮的代码:

连接服务器按钮事件
 1        //客户端 负责 接受 服务端发的数据消息的 线程
 2         Thread threadReceive = null;
 3 
 4         //客户端套接字
 5         Socket socketClient = null;
 6         //连接服务器
 7         private void btn_ConnServer_Click(object sender, EventArgs e)
 8         {
 9             //获取IP
10             IPAddress ip = IPAddress.Parse(tb_IP.Text.Trim());
11             //新建一个网络节点
12             IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(tb_Port.Text.Trim()));
13             //新建一个Socket 负责 监听服务器的通信
14             socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
15             //连接远程主机
16             socketClient.Connect(endPoint);
17 
18             //打印输出
19             ShowMsg("=====================服 务 器 连 接 成 功======================");
20 
21             //创建线程 监听服务器 发来的消息
22             threadReceive = new Thread(RecMsg);
23             //设置为后台线程
24             threadReceive.IsBackground = true;
25             //开启线程
26             threadReceive.Start();
27         }

经过了服务器端代码的讲解之后,看客户端的代码应该轻松多了把。。这里同样我们先获取到IP和Port,这里的IP和Port必须和服务器端的一致,当然也有可能服务器的IP是任意IP,这时Port必须和服务器的一致,然后根据IP和Port建立一个网络节点,然后新建一个Socket对象再连接到远程主机,代码和初级版中一样,同样,我们在客户端也利用了线程来持续监听服务器端发来的消息,线程代码一样,不再做赘述,同样是监听RecMsg()方法,那就贴上RecMsg()方法的代码:

RecMsg
 1        //监听 服务器端 发来的消息
 2         void RecMsg()
 3         {
 4             while (true)
 5             {
 6                 //初始化一个 1M的 缓存区(字节数组)
 7                 byte[] data = new byte[1024 * 1024];
 8                 //将接受到的数据 存放到data数组中 返回接受到的数据的实际长度
 9                 int receiveBytes = socketClient.Receive(data);
10                 //将字符串转换成字节数组
11                 string strMsg = Encoding.UTF8.GetString(data, 0, receiveBytes);
12                 //打印输出
13                 ShowMsg("接受数据:" + strMsg);
14             }
15         }

代码不多加解释了,和服务器端的一样,差别就是服务器端需要Accetp()方法来监听,然后返回个Socket对象,然后由这个对象来进行通信,客户端就直接用一开始的Socket对象进行通信就可以了,用这个对象首发数据,这里为接收数据,while循环实现,这里也是非主线程操作主线程的控件,所以同样要在窗体构造函数中添加关闭跨线程检测代码:

窗体构造函数
1         public FormServer()
2         {
3             InitializeComponent();
4 
5             //关闭对TextBox的跨线程检测
6             TextBox.CheckForIllegalCrossThreadCalls = false;
7         }

ShowMsg()方法,追加文本:

ShowMsg方法
1        //打印输出数据
2         void ShowMsg(string msg)
3         {
4             tb_ShowMsg.AppendText(msg + "\r\n");
5         }

下面我们要通过点击【发送】按钮实现发送数据,贴上代码:

发送按钮事件
1       //发送按钮事件
2         private void btn_SendMsg_Click(object sender, EventArgs e)
3         {
4             //调用Send()方法
5             Send();
6         }

Send()方法代码:

Send方法
 1       //发送数据
 2         void Send()
 3         {
 4             string Message = tb_InputMsg.Text.Trim();
 5 
 6             //将字符串转换成字节数组
 7             byte[] data = System.Text.Encoding.UTF8.GetBytes(Message);
 8             //发送数据
 9             socketClient.Send(data, SocketFlags.None);
10 
11             ShowMsg("发送数据:" + Message);
12 
13             //清空输入消息的内容
14             tb_InputMsg.Text = "";
15         }

发送信息也一样,直接通过SocketClient的Send()方法发送消息就可以了,之前写过几次了,应该都熟悉了吧,,这里就不加叙述了,,,

 

哈哈,,这样客户端的搭建也完成了,,激动人心的时刻要来了。。。。。。。

下面我们来看看一步一步把整个程序运行起来的效果吧

第一步:开启服务器

点击【启动服务器】按钮:

可以看到提示:服务器启动成功,这就说明服务器没问题了,,,,

第二步:开启客户端,因为我们要实现的是服务器可以和多个客户端进行通信,所以我们开启三个客户端

点击【连接服务器】,由于这里三个效果一样,就先贴出一个,看效果:

同样提示:服务器连接成功。。。。。

这时服务器端也接收到了来自三个客户端的连接了,看效果图:

第三步:通信 

现在客户端向服务器发送数据:

同样的还有两个客户端也一样,二号:我是二号客户端;三号:我是三号客户端 ,图就不贴了,和一号一样,这样发数据主要是在服务器端看起来有助于我们区别

这时,服务器端就接受到了我们客户端发送过去的数据了,效果如图:

 

这样,,我们就可以确定从客户端向服务器端发送数据没有问题,,,接下来,从服务器向客户端发送数据,这里我们主要是要看看服务器端的数据是不是可以发送到对应的客户端。。。。

服务器发送数据时,我们首先要在“在线列表”中选择一个客户端进行发送,如图:

这里我们选择了IP为127.0.0.1 端口为2966的客户端(也就是刚才的二号客户端),向其发送:“你好,我是服务器” 数据,我们看效果:

一号:

二号:

三号:

 

可以看出,我们的程序没什么问题,,数据发送到了对应的客户端了,,,,这样就达到写这个程序的目的了,本次博客的任务也就完成了,哈哈,,,当然这个程序还是有很大的扩展的,比如现在只是实现文字消息的传输,也可以扩展成文件传输,图片传输等,,,,欢迎各位扩展交流啊。。。。。。。

 

 

 

 

 

posted @ 2012-07-18 20:39  HolyKnight  阅读(28721)  评论(46编辑  收藏  举报