你的位置:首页 > ASP.net教程

[ASP.net教程]Socket实现仿QQ聊天(可部署于广域网)附源码(2)


1.前言

     这是本系列的第二篇文章,第一篇文章得到了很多朋友们的支持,在这里表示非常的感谢。对于这一系列文章需要补充的是这只是一篇入门级别的Socket通信文章,对于专业人员来说完全可以跳过。本文只介绍一些基本TCP通信技术并使用该技术实现聊天功能。本篇文章实现聊天服务器搭建,我会把聊天服务器部署到广域网服务器上,到时候大家就可以可以在源码里面打开客户端与我聊天啦!(这只是一个初级版功能简单不支持离线消息,所以聊天的前提是我在线(用户名为张三的就是我,Q我吧)……)。

2.本篇实现功能

1. 聊天室服务器端的创建。

2. 聊天室客户端的创建。

3. 实现客户与服务器的连接通讯。

4. 实现客户之间的私聊。

3.具体实现

(1)客户端搭建

1)运行过程 与服务端建立连接—>首次连接向服务器发送登录用户信息(格式例如 张三| )—>聊天:先将聊天消息发送到服务器,然后由服务器解析发给好友(发往服务器的消息如下 张三|李四|你好呀李四?),如图

QQ截图20160422200750

客户端代码实现:

 1 //客户端通信套接字 2   private Socket clientSocket; 3   //新线程 4   private Thread thread; 5   //当前登录的用户 6   private string userName = ""; 7   public Client() 8    { 9      InitializeComponent(); 10     //防止新线程调用主线程卡死 11     CheckForIllegalCrossThreadCalls = false; 12    } 13  14   //通过IP地址与端口号与服务端建立链接    15   private void btnToServer_Click(object sender, EventArgs e) 16    { 17     //连接服务器前先选择用户 18     if (cmbUser.SelectedItem == null) 19      { 20       MessageBox.Show("请选择登录用户"); 21       return; 22      } 23     userName = cmbUser.SelectedItem.ToString(); 24     this.Text = "当前用户:" + userName; 25     //登录后禁止切换用户 26     cmbUser.Enabled = false; 27     //加载好友列表 28     foreach (object item in cmbUser.Items) 29      { 30       if (item != cmbUser.SelectedItem) 31        { 32          lbFriends.Items.Add(item); 33        } 34      } 35     //新建通信套接字 36     clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 37     //这里的ip地址,端口号都是服务端绑定的相关数据。 38     IPAddress ip = IPAddress.Parse(txtIP.Text); 39     var endpoint = new IPEndPoint(ip, Convert.ToInt32(txtPort.Text)); 40     try 41      { 42     clientSocket.Connect(endpoint); //链接有端口号与IP地址确定服务端. 43     //登录时给服务器发送登录消息例如张三|  44       string str = userName + "|" + " "; 45       byte[] buffer = Encoding.UTF8.GetBytes(str); 46        clientSocket.Send(buffer); 47      } 48     catch 49      { 50       MessageBox.Show("与服务器连接失败"); 51        lbFriends.Items.Clear(); 52      } 53     //客户端在接受服务端发送过来的数据是通过Socket 中的Receive方法,该方法会阻断线程,所以我们自己为该方法创建了一个线程 54     thread = new Thread(ReceMsg); 55     thread.IsBackground = true; //设置后台线程 56      thread.Start(); 57    } 58  59   public void ReceMsg() 60    { 61     while (true) 62      { 63  64       try 65        { 66         var buffer = new byte[1024 * 1024 * 2]; 67         int dateLength = clientSocket.Receive(buffer); //接收服务端发送过来的数据 68         //把接收到的字节数组转成字符串显示在文本框中。 69         string ReceiveMsg = Encoding.UTF8.GetString(buffer, 0, dateLength); 70         string[] msgTxt = ReceiveMsg.Split('|'); 71         string newstr ="   "+msgTxt[0] +":我"+ "\r\n"+"   " + msgTxt[2] + "      ____[" + DateTime.Now +"]" + "\r\n" + "\r\n"; 72          ShowSmsg(newstr); 73        } 74       catch 75        { 76        77        } 78      } 79    } 80  81   private void btnSend_Click(object sender, EventArgs e) 82    { 83     if (lbFriends.SelectedItems.Count != 1) 84      { 85       MessageBox.Show("请选择好友"); 86       return; 87      } 88     string friend = lbFriends.SelectedItem.ToString(); 89     try 90      { 91       //界面显示消息 92       string newstr = "我" + ":" + friend + "\r\n" + txtMsg.Text.Trim() + "      ____[" + DateTime.Now + 93               "]" + "\r\n" + "\r\n"; 94        ShowSmsg(newstr); 95       //发往服务器的消息   格式为 (发送者|接收者|信息) 96       string str = userName + "|" + friend + "|" + txtMsg.Text.Trim(); 97       //将消息转化为字节数据传输 98       byte[] buffer = Encoding.UTF8.GetBytes(str); 99        clientSocket.Send(buffer);100       txtMsg.Text = "";101      }102     catch103      {104       MessageBox.Show("与服务器连接失败");105      }106    }107   //展示消息108   private void ShowSmsg(string newStr)109    {110      txtChat.AppendText(newStr);111    }112   private void btnCloseSer_Click(object sender, EventArgs e)113    {114      clientSocket.Close();115   } 

View Code

 


 

 

(2)服务器端搭建

     我们上篇讲到聊天服务器与单个客户端实现通信,服务器通信的socket搭建后,开启新的线程来监听是否有客户端连入,为了实现后期的客户端对客户端的通信我们首先要存储客户端的socket的IP与端口号,以及用户名信息,服务器接收到消息后将消息解析转发。我实现的思路如下:

(0)服务器页面搭建,如下图

图片1

服务器代码:

 1 //客户端通信套接字 2   private Socket clientSocket; 3   //新线程 4   private Thread thread; 5   //当前登录的用户 6   private string userName = ""; 7   public Client() 8    { 9      InitializeComponent(); 10     //防止新线程调用主线程卡死 11     CheckForIllegalCrossThreadCalls = false; 12    } 13  14   //通过IP地址与端口号与服务端建立链接    15   private void btnToServer_Click(object sender, EventArgs e) 16    { 17     //连接服务器前先选择用户 18     if (cmbUser.SelectedItem == null) 19      { 20       MessageBox.Show("请选择登录用户"); 21       return; 22      } 23     userName = cmbUser.SelectedItem.ToString(); 24     this.Text = "当前用户:" + userName; 25     //登录后禁止切换用户 26     cmbUser.Enabled = false; 27     //加载好友列表 28     foreach (object item in cmbUser.Items) 29      { 30       if (item != cmbUser.SelectedItem) 31        { 32          lbFriends.Items.Add(item); 33        } 34      } 35     //新建通信套接字 36     clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 37     //这里的ip地址,端口号都是服务端绑定的相关数据。 38     IPAddress ip = IPAddress.Parse(txtIP.Text); 39     var endpoint = new IPEndPoint(ip, Convert.ToInt32(txtPort.Text)); 40     try 41      { 42     clientSocket.Connect(endpoint); //链接有端口号与IP地址确定服务端. 43     //登录时给服务器发送登录消息例如张三|  44       string str = userName + "|" + " "; 45       byte[] buffer = Encoding.UTF8.GetBytes(str); 46        clientSocket.Send(buffer); 47      } 48     catch 49      { 50       MessageBox.Show("与服务器连接失败"); 51        lbFriends.Items.Clear(); 52      } 53     //客户端在接受服务端发送过来的数据是通过Socket 中的Receive方法,该方法会阻断线程,所以我们自己为该方法创建了一个线程 54     thread = new Thread(ReceMsg); 55     thread.IsBackground = true; //设置后台线程 56      thread.Start(); 57    } 58  59   public void ReceMsg() 60    { 61     while (true) 62      { 63  64       try 65        { 66         var buffer = new byte[1024 * 1024 * 2]; 67         int dateLength = clientSocket.Receive(buffer); //接收服务端发送过来的数据 68         //把接收到的字节数组转成字符串显示在文本框中。 69         string ReceiveMsg = Encoding.UTF8.GetString(buffer, 0, dateLength); 70         string[] msgTxt = ReceiveMsg.Split('|'); 71         string newstr ="   "+msgTxt[0] +":我"+ "\r\n"+"   " + msgTxt[2] + "      ____[" + DateTime.Now +"]" + "\r\n" + "\r\n"; 72          ShowSmsg(newstr); 73        } 74       catch 75        { 76        77        } 78      } 79    } 80  81   private void btnSend_Click(object sender, EventArgs e) 82    { 83     if (lbFriends.SelectedItems.Count != 1) 84      { 85       MessageBox.Show("请选择好友"); 86       return; 87      } 88     string friend = lbFriends.SelectedItem.ToString(); 89     try 90      { 91       //界面显示消息 92       string newstr = "我" + ":" + friend + "\r\n" + txtMsg.Text.Trim() + "      ____[" + DateTime.Now + 93               "]" + "\r\n" + "\r\n"; 94        ShowSmsg(newstr); 95       //发往服务器的消息   格式为 (发送者|接收者|信息) 96       string str = userName + "|" + friend + "|" + txtMsg.Text.Trim(); 97       //将消息转化为字节数据传输 98       byte[] buffer = Encoding.UTF8.GetBytes(str); 99        clientSocket.Send(buffer);100       txtMsg.Text = "";101      }102     catch103      {104       MessageBox.Show("与服务器连接失败");105      }106    }107   //展示消息108   private void ShowSmsg(string newStr)109    {110      txtChat.AppendText(newStr);111    }112   private void btnCloseSer_Click(object sender, EventArgs e)113    {114      clientSocket.Close();115   } 

View Code

 

 

(1)当两个不同客户端的连入,生成两个通信套接字1,2。这时为了与客户端实现通信我们有必要建立一个客户端管理类,来存储客户端的信息。

(2)用户名与客户端通信的socket的IP与端口号对应,以Dictionary字典形式存入键:IP与端口号 ,值:用户名(这里为演示原理所以没加入数据库,只是模拟,下一章再加入数据库);当用户第一次连入,我们必须记录他的IP并与用户对应起来,如果局域网聊天IP在同一网段两个客户端还可以互相找到, 如果广域网下两个客户端只有通过服务器转接才能找到。

(3)声明一个全局消息委托 public delegate void DGSendMsg(string strMsg);

我的思路如下图:

QQ截图20160422200842

下面是我写的客户端管理类:

image

 1 public class ClientManager 2  { 3    //好友列表控件 4    private System.Windows.Forms.ListBox listClient; 5    //在服务器上显示消息的委托(全局) 6    DGSendMsg dgSendMsg; 7    //通信套接字key :客户端ip value :对应的通信套接字 8    private Dictionary<string, Socket> ClientSocket; 9    // 通信套接字key :客户端ip value :用户名(由于没有添加数据库我们暂时这么模拟,下篇我会讲到) 10    private Dictionary<string, string> UserSocket; 11    public ClientManager() 12    { } 13    /// <summary> 14    /// 客户端管理类 15    /// </summary> 16    /// <param name="lb">列表控件</param> 17    /// <param name="dgSendMsg">显示消息</param> 18    public ClientManager(System.Windows.Forms.ListBox lb, DGSendMsg dgSendMsg) 19    { 20      //用户列表 21      this.listClient = lb; 22      //消息委托 23      this.dgSendMsg = dgSendMsg; 24      //通信字典 25      ClientSocket = new Dictionary<string, Socket>(); 26      //用户字典 27      UserSocket = new Dictionary<string, string>(); 28    } 29    #region 添加客户端通信套接字+ public void AddClient(Socket sokMsg) 30    /// <summary> 31    /// 添加通信套接字 32    /// </summary> 33    /// <param name="sokMag">负责通信的socket</param> 34    public void AddClient(Socket sokMsg) 35    { 36      //获取客户端通信套接字 37      string strEndPoint = sokMsg.RemoteEndPoint.ToString(); 38      //通信套接字加入字典 39      ClientSocket.Add(strEndPoint, sokMsg); 40      //sokServer.Accept()这个接收消息的方法会使线程卡死,所以要开启新线程 41      Thread thrMag = new Thread(ReciveMsg); 42      //设置为后台线程防止卡死 43      thrMag.IsBackground = true; 44      //开启线程 为新线程传入通信套接字参数 45      thrMag.Start(sokMsg); 46      dgSendMsg(strEndPoint + "成功连接上服务端~~~!" + DateTime.Now.ToString()); 47    } 48    #endregion 49    bool isReceing = true; 50    #region void ReciveMsg(object sokMsgObj)  服务接收客户端消息 51    /// <summary> 52    /// 服务接收客户端消息 53    /// </summary> 54    /// <param name="sokMsgObj">客户端Scoket</param> 55    void ReciveMsg(object sokMsgObj) 56    { 57      Socket sokMsg = null; 58      try 59      { 60        //sokMsg接收消息的socket 61        sokMsg = sokMsgObj as Socket; 62        //创建接收消息的缓冲区  默认5M 63        byte[] arrMsg = new byte[5 * 1024 * 1024]; 64        //循环接收 65        while (isReceing) 66        { 67          //接收到的真实消息存入缓冲区   并保存消息的真实长度(因为5M缓冲区不会全部用掉) 68          int realLength = sokMsg.Receive(arrMsg); 69          //将缓冲区的真实数据转成字符串 70          string strMsg = Encoding.UTF8.GetString(arrMsg, 0, realLength); 71          //dgSendMsg(strMsg); 72    73          string[] msgTxt = strMsg.Split('|'); 74          // msgTxt.Length == 2说明用户第一次连入,我们必须记录他的IP并与用户对应起来,如果局域网聊天 75          //IP在同一网段两个客户端还可以互相找到, 如果广域网下只有通过服务器转接才能找到 76          if (msgTxt.Length == 2) 77          { 78            //如果用户名已登录则强制下线 79            if (UserSocket.Values.Contains(msgTxt[0])) 80            { 81              sokMsg.Close(); 82              return; 83            } 84            UserSocket.Add(sokMsg.RemoteEndPoint.ToString(), msgTxt[0]); 85            //显示列表 86            listClient.Items.Add(sokMsg.RemoteEndPoint + "---" + msgTxt [0]+ @"---上线~\(≧▽≦)/~啦啦啦"); 87            continue; 88          } 89  90           //发送信息给客户端 91          SendMsgToClient(strMsg); 92        } 93      } 94      catch 95      { 96        //连接出错说明客户端连接断开 97        RemoveClient(sokMsg.RemoteEndPoint.ToString()); 98      } 99    }100    #endregion101    /// <summary>102    /// 移除下线用户信息103    /// </summary>104    /// <param name="strClientID">IP:端口</param>105    public void RemoveClient(string strClientID)106    {107      //要移除的用户名108      string name = UserSocket[strClientID];109      // 要移除的在线管理的项110      string onlineStr = strClientID + "---" + UserSocket[strClientID] + @"---上线~\(≧▽≦)/~啦啦啦";111   112      //移除在线管理listClient 集合113      if (listClient.Items.Contains(onlineStr))114      {115        listClient.Items.Remove(onlineStr);116      }117      //断开socket连接118      if (ClientSocket.Keys.Contains(strClientID))119      {120        ClientSocket[strClientID].Close();121        ClientSocket.Remove(strClientID);122        UserSocket.Remove(strClientID);123      }124      dgSendMsg(strClientID + "---" + name + @"---下线_"+DateTime.Now);125    }126    public void SendMsgToClient(string Msg)127    {128      //解析发来的数据129 130      string[] msgTxt = Msg.Split('|');131 132      //不可解析数据133      if (msgTxt.Length < 2)134      {135        return;136      }137      // 解析收消息的用户名138      string strTo = msgTxt[1];139      //获得当前发送的用户140      var nowtouser = UserSocket.Where(w => w.Value == strTo).FirstOrDefault();141      if (nowtouser.Key != null)142      {143        byte[] buffer = System.Text.Encoding.UTF8.GetBytes(Msg);144        Socket conn = ClientSocket[nowtouser.Key];145        conn.Send(buffer);146      }147 148    }149  }

View Code

 

 

效果:聊天
1234

(4)总结

          本次实现了客户端对客户端的一对一聊天(本篇不涉及数据库),实现思路大体为:客户端1将消息发给服务器,服务器解析消息把消息发给客户端2。下一篇我们讲自定义协议发送文件,窗口抖动,以及各种文件格式的接收的解决思路。最后你可以打开源码的客户端,登录张三以外的客户端给我发消息,我这边登录的是张三的账户,赶快试一下吧!!!

这个系列未完,待续。。。。。。。。。。。。。。。。。。。。。,期待您的关注

本次源码地址:http://pan.baidu.com/s/1eRPAZvk