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          catch
103          {
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          catch
103          {
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       #endregion
101       /// <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。下一篇我们讲自定义协议发送文件,窗口抖动,以及各种文件格式的接收的解决思路。最后你可以打开源码的客户端,登录张三以外的客户端给我发消息,我这边登录的是张三的账户,或者打开两个客户端自己聊天(不需要运行服务端,默认是我的服务器IP,理论上有网就可以聊天),赶快试一下吧!!!

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

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

posted @ 2016-04-22 21:20  ATtuing  阅读(8473)  评论(1编辑  收藏  举报