posts - 710,  comments - 81,  views - 260万
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

1.前言

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

2.本篇实现功能

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

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

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

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

3.具体实现

(1)客户端搭建

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

QQ截图20160422200750

客户端代码实现:

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

  

(2)服务器端搭建

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

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

图片1

服务器代码:

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

  

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

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

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

我的思路如下图:

QQ截图20160422200842

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

image

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

  

效果:聊天 
1234

(4)总结

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

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

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

 

转载自:http://www.cnblogs.com/ATtuing/p/5422628.html

 

程序员的基础教程:菜鸟程序员

posted on   itprobie-菜鸟程序员  阅读(761)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示