[toc]

简述

关于Socket的原理我就不在这里赘述了,有大佬已经作详细的说明了:

Socket原理讲解

因为网上大多介绍的是在一台PC端使用虚拟服务器和本机进行通信,本质还是内网通信。

这里要介绍的是怎么用Socket进行公网通信,也就是在不同的局域网之间通信。其实代码实现和内网通信大差不差,重点区别在于创建监听Socket时,绑定或连接的IP的正确性显得尤为重要,一旦设置错误,连接就会失败!

在这里先简单讲一下计网知识,对于服务器来说,在它和同一内网的设备看来,它的IP是内网IP;对于处在不同局域网的设备看来,它的IP是公网IP。

因此,

服务器端:因为是要放到服务器上的,创建监听Socket用的IP为服务器自身的内网IP。

客户端:用Socket连接时,要连接服务器端的公网IP。

接下来我们直接上结果展示和代码吧!

功能演示视频(b站)

C#+Socket 聊天室(公网通信 客户端-服务器端-客户端)

准备工作

  1. 一台云服务器(推荐阿里云、腾讯云等大厂的,轻量最低配即可),windows系统的,安全组设置开放如下端口:

image-20220115110724263

说明:

服务器安全组放通3389端口,才能进行远程登录。

再放通我们要用来进行通信的50000端口。

把写好的服务器端程序形成的可执行文件复制到服务器启动即可。

  1. 一台或多台PC(可实现多PC同时在线聊天)

服务器端

服务器端界面

image-20220115113311377

服务器端代码

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Socket_Server
{
public partial class ServerForm : Form
{
List<Socket> ClientSocketList = new List<Socket>();
public ServerForm()
{
InitializeComponent();
}
private void btnStart_Click(object sender, EventArgs e)
{
//1. 创建一个负责监听的Socekt
Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//绑定端口IP(服务器内网)
//创建IP地址和端口号对象
IPAddress ip = IPAddress.Parse(txtIP.Text);
IPEndPoint point = new IPEndPoint(ip, Convert.ToInt32(txtPort.Text));
//让负责监听的Socekt绑定IP地址和端口号
try
{
socketWatch.Bind(point);
}
catch (Exception ex)
{
MessageBox.Show("无法启动服务器:" + ex.Message);
return;
}
btnStart.Enabled = false;
//设置监听队列
socketWatch.Listen(20);
//创建一个新线程执行监听程序
Thread th = new Thread(Listen);
th.IsBackground = true;
th.Start(socketWatch);
}
private void ShowMsg(string str)
{
txtLog.AppendText(str);
txtLog.AppendText("\r\n");
}
//这个Listen是自定义的方法
#region 监听线程
private void Listen(object o)
{
Socket socketWatch = o as Socket;
ShowMsg("服务器端开始接收客户端的连接!");
while (true)
{
Socket proxSocket = socketWatch.Accept(); //阻塞进程直到有客户端连接
ShowMsg(string.Format("客户端:{0}上线了!", proxSocket.RemoteEndPoint.ToString()));
//开启一个不断接收客户端的新线程
Thread th = new Thread(ReceiveData);
th.IsBackground = true;
th.Start(proxSocket);
}
}
#endregion
//服务器端不停地接收客户端发送过来的消息
private void ReceiveData(object o)
{
Socket proxsocket = o as Socket;
SendMsgForAll(string.Format("客户端:{0}上线了!", proxsocket.RemoteEndPoint.ToString()));
ClientSocketList.Add(proxsocket);
while (true)
{
byte[] data = new byte[1024 * 1024];
//当客户端连接成功后,服务器应该接收客户端发来的消息
//获取收到的数据的字节数
int len = 0;
try
{
len = proxsocket.Receive(data, 0, data.Length, SocketFlags.None);
}
catch (Exception ex)
{
if (ClientSocketList.Contains(proxsocket))
{
//异常退出
ClientSocketList.Remove(proxsocket);
ShowMsg(string.Format("客户端:{0}非正常退出!", proxsocket.RemoteEndPoint.ToString()));
SendMsgForAll(string.Format("客户端:{0}非正常退出!", proxsocket.RemoteEndPoint.ToString()));
return;
}
}
//客户端正常退出
if (len <= 0)
{
if (ClientSocketList.Contains(proxsocket))
{
ClientSocketList.Remove(proxsocket);
ShowMsg(string.Format("客户端[{0}]正常退出!", proxsocket.RemoteEndPoint.ToString()));
SendMsgForAll(string.Format("客户端[{0}]正常退出!", proxsocket.RemoteEndPoint.ToString()));
}
return;
}
#region 接收到的是字符串
if (data[0] == 1)
{
//开启字符串转发线程
objClass stringobj = new objClass();
stringobj.objSocket = proxsocket;
stringobj.objData = data;
Thread stringth = new Thread(new ParameterizedThreadStart(TransReceiveStringAll));
stringth.Start(stringobj);
}
#endregion
#region 接收到的是“戳一戳”
else if (data[0] == 2)
{
foreach (var proxSocket in ClientSocketList)
{
if (proxsocket.Connected&&proxSocket!=proxsocket)
{
proxSocket.Send(new byte[] { 2 }, SocketFlags.None);
}
}
}
#endregion
}
}
#region 转发接收到的字符串
private void TransReceiveStringAll(object o)
{
objClass result = o as objClass;
Socket proxSocket = result.objSocket;
byte[] data = result.objData;
string strTmp = ProcessReceiveString(data);
strTmp = string.Format("客户端[" + proxSocket.RemoteEndPoint.ToString() +"]"+":"+ strTmp);
ShowMsg(strTmp);
if (ClientSocketList.Contains(proxSocket))
{
foreach(Socket socketTmp in ClientSocketList)
{
if(socketTmp != proxSocket)
{
SendMsg(socketTmp, strTmp);
}
}
}
}
#endregion
#region 处理接收到的字符串
private string ProcessReceiveString(byte[] data)
{
//把实际的字符串拿到
string str = Encoding.Default.GetString(data,1,data.Length-1);
return str;
}
#endregion
#region 发送字符串消息
private void SendMsg(Socket socketTmp, string Msg)
{
//原始字符串转成字节数组
byte[] data = Encoding.Default.GetBytes(Msg);
//对原始的数据数组加上协议的头部字节
byte[] result = new byte[data.Length + 1];
//设置当前的协议头部字节是1:代表字符串
result[0] = 1;
//把原始的数据放到最终的字节数组里去
Buffer.BlockCopy(data, 0, result, 1, data.Length);
socketTmp.Send(result, 0, result.Length, SocketFlags.None);
}
#endregion
#region 给所有当前连接上的客户端发送字符串消息
private void SendMsgForAll(string Msg)
{
foreach (var socketTmp in ClientSocketList)
{
if (socketTmp.Connected)
{
SendMsg(socketTmp, Msg);
}
}
}
#endregion
#region 服务器端发送消息
private void btnSendMsg_Click(object sender, EventArgs e)
{
ShowMsg("服务器端:"+txtMsg.Text);
SendMsgForAll("服务器端:" + txtMsg.Text);
txtMsg.Clear();
txtMsg.Focus();
}
#endregion
#region 戳一戳
private void btnShock_Click(object sender, EventArgs e)
{
foreach (var proxSocket in ClientSocketList)
{
if (proxSocket.Connected)
{
proxSocket.Send(new byte[] { 2 }, SocketFlags.None);
}
}
}
#endregion
private void ServerForm_Load(object sender, EventArgs e)
{
Control.CheckForIllegalCrossThreadCalls = false;
}
}
}
class objClass
{
public Socket objSocket;
public byte[] objData;
}

客户端

客户端界面

image-20220115113438093

客户端代码

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ClientForm
{
public partial class ClientForm : Form
{
public Socket ClientSocekt { get; set; }
public ClientForm()
{
InitializeComponent();
ConnectInit();
}
public void ConnectInit()
{
//客户端链接服务器端
//1. 创建Socekt对象
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
ClientSocekt = socket;
//2. 链接服务器端
try
{
//填写要连接服务器的公网ip和端口号
IPAddress iPAddress = IPAddress.Parse("121.4.211.53");
socket.Connect(iPAddress, 50000);
}
catch (Exception ex)
{
MessageBox.Show("连接失败,请重新连接");
return;
}
ShowMsg("连接服务器成功!");
//3. 发送消息 接收消息
Thread th = new Thread(ReceiveData);
th.IsBackground = true;
th.Start(ClientSocekt);
}
public void ShowMsg(string str)
{
txtLog.AppendText(str);
txtLog.AppendText("\r\n");
}
public void ReceiveData(object o)
{
Socket proxSocket = o as Socket;
while (true)
{
byte[] data = new byte[1024 * 1024];
//客户端连接成功后,服务器应该接收客户端发来的消息
//获取收到数据的字节数
int len = 0;
try
{
len = proxSocket.Receive(data, 0, data.Length, SocketFlags.None);
}
catch (Exception ex)
{
//异常退出
try
{
ShowMsg(string.Format("服务器端非正常退出!"));
}
catch (Exception ex1)
{
}
StopConnect();
return;
}
//服务端正常退出
if (len <= 0)
{
try
{
ShowMsg(string.Format("服务器端正常退出!"));
}
catch (Exception ex2)
{
}
//关闭链接
StopConnect();
return;
}
//接收到的数据中的第一个字节 1:字符串,2:闪屏,3:文件
#region 接收到的是字符串
if (data[0] == 1)
{
string strMsg = ProcessRecieveString(data);
ShowMsg(strMsg);
}
#endregion
#region 接收到的是闪屏
else if (data[0] == 2)
{
shock();
}
#endregion
}
}
private void StopConnect()
{
try
{
if (ClientSocekt.Connected)
{
ClientSocekt.Shutdown(SocketShutdown.Both);
//超过100s未关闭成功则强行关闭
ClientSocekt.Close(100);
}
}
catch (Exception ex)
{
}
}
#region 处理接收到的字符串ProcessRecieveString(byte[] data)
public string ProcessRecieveString(byte[] data)
{
//把实际的字符串拿到
string str = Encoding.Default.GetString(data, 1, data.Length - 1);
return str;
}
#endregion
#region 闪屏方法shock()
private void shock()
{
//把窗体最原始的坐标记住
Point oldLocation = this.Location;
Random r = new Random();
for (int i = 0; i < 50; i++)
{
this.Location = new Point(r.Next(oldLocation.X - 5, oldLocation.X), r.Next(oldLocation.Y, oldLocation.Y));
Thread.Sleep(50);
this.Location = oldLocation;
}
}
#endregion
private void btnSendMsg_Click(object sender, EventArgs e)
{
if (ClientSocekt.Connected)
{
ShowMsg("我:"+txtMsg.Text);
//原始字符串转成字节数组
byte[] data = Encoding.Default.GetBytes(txtMsg.Text);
//对原始的数据数组加上协议的头部字节
byte[] result = new byte[data.Length + 1];
//设置当前的协议头部字节是1:代表字符串
result[0] = 1;
//把原始的数据放到最终的字节数组里去
Buffer.BlockCopy(data, 0, result, 1, data.Length);
ClientSocekt.Send(result, 0, result.Length, SocketFlags.None);
txtMsg.Clear();
txtMsg.Focus();
}
else
{
ShowMsg("发送失败,未连接服务器!");
}
}
private void btnShock_Click(object sender, EventArgs e)
{
ClientSocekt.Send(new byte[] { 2 }, SocketFlags.None);
}
private void ClientForm_Load(object sender, EventArgs e)
{
Control.CheckForIllegalCrossThreadCalls = false;
}
private void ClientForm_FormClosed(object sender, FormClosedEventArgs e)
{
StopConnect();
System.Environment.Exit(0);
}
}
}

总结

个人认为公网通信相较于内网通信更具有普遍应用意义,虽然博主是通信专业的学生,但是计网还没深入学习,于是找了各种资料加上自身对计网的理解,终于在内网通信的基础上实现了公网的聊天应用,大家可根据自己的喜好DIY一个自己的聊天室,这篇博客主要是让大家了解怎么实现客户端-服务器端-客户端的网络通信。

工程文件下载

链接:https://pan.baidu.com/s/1vl7I0Rk0zkgJUmLWRLuIdQ
提取码:qy8w