简述
关于Socket的原理我就不在这里赘述了,有大佬已经作详细的说明了:
因为网上大多介绍的是在一台PC端使用虚拟服务器和本机进行通信,本质还是内网通信。
这里要介绍的是怎么用Socket进行公网通信,也就是在不同的局域网之间通信。其实代码实现和内网通信大差不差,重点区别在于创建监听Socket时,绑定或连接的IP的正确性显得尤为重要,一旦设置错误,连接就会失败!
在这里先简单讲一下计网知识,对于服务器来说,在它和同一内网的设备看来,它的IP是内网IP;对于处在不同局域网的设备看来,它的IP是公网IP。
因此,
服务器端:因为是要放到服务器上的,创建监听Socket用的IP为服务器自身的内网IP。
客户端:用Socket连接时,要连接服务器端的公网IP。
接下来我们直接上结果展示和代码吧!
功能演示视频(b站)
C#+Socket 聊天室(公网通信 客户端-服务器端-客户端)
准备工作
- 一台云服务器(推荐阿里云、腾讯云等大厂的,轻量最低配即可),windows系统的,安全组设置开放如下端口:
说明:
服务器安全组放通3389端口,才能进行远程登录。
再放通我们要用来进行通信的50000端口。
把写好的服务器端程序形成的可执行文件复制到服务器启动即可。
- 一台或多台PC(可实现多PC同时在线聊天)
服务器端
服务器端界面
服务器端代码
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;
}
客户端
客户端界面
客户端代码
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一个自己的聊天室,这篇博客主要是让大家了解怎么实现客户端-服务器端-客户端的网络通信。