1.什么是Socket?
在计算机领域socket被翻译为套接字,它是计算机之间进行通信的一种方式,通过socket这种约定,一台计算机可以向另外一台计算机发送数据和接收数据。
2.Socket的本质?
Socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是socket编程接口。
3.socket的作用?
可以实现不同虚拟机或者是计算机之间的通信。
4.socket的典型应用?
(1)socket的典型应用之一 就是web服务器和浏览器,浏览器获取用户输入的URL,向服务器发起请求,服务器分析接收到的URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、
图片、视频等元素呈现给用户。
(2)QQ或者微信等聊天工具也是socket的应用之一,本地的QQ或者微信程序就是客户端,登录过程就是连接服务器的过程,聊天过程其实就是socket的发送和接收过程。
socket主要包括以下几种接口:
socket位于应用层和传输层之间,把socket比作门,门外是邮局,你要送信就要通过门,把信从门送出去到邮局,然后由邮局帮你送达目标的门,目的地主任再打开门,从门取出来邮局送过来的信,上述比喻
中,邮局就是传输层(及更下面的层),而门内就是应用。
socket的编程方式?
socket,一切皆文件,都可以用"打开open------>读写read/write------->close"模式来操作。socket就是该模式的一个实现,socket即是一个特殊的文件,一些socket函数就是对其进行的操作(读/写IO,打开,关闭)
因此socket提供了类似于连接(Connect),关闭(Close),发送,接收等方法调用。
数据的传输方式:STREAM和DREAM
(1)STREAM表示面向连接的数据传输方式,数据可以准确无误的到达另一台计算机,如果损坏或丢失,可以重新发送,但是效率较慢。
(2)DREAM表示无连接的数据传输方式,计算机只管传输数据,不做数据校验,DREAM所作的校验工作少,所以效率比STREAM高。
QQ视频聊天就是使用DREAM传输数据,因为首先要保证通信效率,尽可能减小延迟,而数据的准确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点和杂音,不会对通
信质量有质的影响。
接下来,我们将使用socket来开发网络调试助手。
服务器端程序的编写:
第一步,创建一个用于通信的socket套接字
第二步,给已经创建的套接字绑定一个端口号,这个一般通过设置网络套接口地址和调用Bind()函数来实现。
第三步,调用Listen()函数使套接字成为一个监听的套接字。
第四步,调用accept()函数来接收客户端的连接,这时候就可以和客户端通信了。
第五步,处理客户端的连接请求。
第六步,终止连接。
界面搭建:
启动服务器的程序编写:
#region 开启服务
///
///
///
private void btn_StartService_Click(object sender, EventArgs e)
{
// 第一步:调用socket()函数创建一个用于通信的套接字,这里使用基于Tcp的方式
socketSever = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 第二步:给已经创建的套接字绑定一个端口号,这一般通过设置网络套接口地址和调用bind()函数来实现。
IPEndPoint ipe = new IPEndPoint(IPAddress.Parse(this.txt\_IP.Text), int.Parse(this.txt\_Port.Text));
try
{
socketSever.Bind(ipe);
}
catch (Exception ex)
{
//写入日志
AddLog(2, "服务器开启失败:" + ex.Message);
return;
}
// 第三步:调用listen()函数使套接字成为一个监听套接字。这个10表示的是存放在缓冲池的连接数(类似于数据库连接池),并不是最大的连接的客户端数量
//当接收到客户端连接,就会从缓冲区中拿掉一个,缓冲区就会空出一个位置,如果把监听客户端的线程注释掉,那么会发现,缓冲区的位置移植无法释放,
socketSever.Listen(10);
//创建一个监听的线程
Task.Run(new Action(() =>
{
CheckListening();
}));
AddLog(0, "服务器开启成功");
this.btn\_StartService.Enabled = false;
}
#endregion
监听线程
#region 监听线程
/// <summary>
/// 检查监听的线程方法体
/// </summary>
private void CheckListening()
{
while (true)
{
// 第四步:调用accept()函数来接受客户端的连接,这是就可以和客户端通信了,Accept是一个阻塞式的,当有客户端连接上服务器的时候,就会继续向下执行。
Socket socketClient = socketSever.Accept();
string client = socketClient.RemoteEndPoint.ToString();
AddLog(0, client + "上线了");
//添加该客户端到字典中
CurrentClientlist.Add(client, socketClient);
UpdateOnline(client, true);
Task.Run(new Action(() =>
{
ReceiveMessage(socketClient);
}));
}
}
#endregion
接收来自客户端的消息
#region 多线程接收数据
///
///
private void ReceiveMessage(Socket socketClient)
{
while (true)
{
// 创建一个10M的缓冲区
byte\[\] buffer = new byte\[1024 \* 1024 \* 10\];
int length = -1;
string client = socketClient.RemoteEndPoint.ToString();
// 第五步:处理客户端的连接请求。
try
{
//Receive也是一个阻塞式的方法,当接收到消息后,将会继续执行。接收到的数据将会放到缓冲区buffer中
//如果客户端断线,一个将会进入try catch,另外一个会进入接收的length为0
length = socketClient.Receive(buffer);
}
catch (Exception)
{
//客户端下线
UpdateOnline(client, false);
AddLog(0, client + "下线了");
//从字典中移除
CurrentClientlist.Remove(client);
break;
}
if (length > 0)
{
string msg = string.Empty;
MessageType type = (MessageType)buffer\[0\];
//客户端发送消息时,首个字节表示的是发送的数据的类型,实际真正的数据长度是length-1,根据首字节来判断接收数据的类型
switch (type)
{
//接收的是ASCLL
case MessageType.ASCII:
msg = Encoding.ASCII.GetString(buffer, 1, length - 1);
AddLog(0, client + ":" + msg);
break;
//接收UTF8
case MessageType.UTF8:
msg = Encoding.UTF8.GetString(buffer, 1, length - 1);
AddLog(0, client + ":" + msg);
break;
//接收十六进制
case MessageType.Hex:
msg = HexGetString(buffer, 1, length - 1);
AddLog(0, client + ":" + msg);
break;
//接收文件
case MessageType.File:
Invoke(new Action(() =>
{
SaveFileDialog sfd = new SaveFileDialog();
sfd.Filter = "txt files(\*.txt)|\*.txt|xls files(\*.xls)|\*.xls|xlsx files(\*.xlsx)|\*.xlsx|All files(\*.\*)|\*.\*";
if (sfd.ShowDialog() == DialogResult.OK)
{
string fileSavePath = sfd.FileName;
using (FileStream fs = new FileStream(fileSavePath, FileMode.Create))
{
//将缓冲区的数据写入
fs.Write(buffer, 1, length - 1);
}
AddLog(0, "文件成功保存至" + fileSavePath);
}
}));
break;
//接收JSON
case MessageType.JSON:
Invoke(new Action(() =>
{
string res = Encoding.Default.GetString(buffer, 1, length);
List<Student> StuList = JSONHelper.JSONToEntity<List<Student>>(res);
new FrmJSON(StuList).Show();
AddLog(0, "接收JSON数据:" + res);
}));
break;
default:
break;
}
}
//length<=0,也视为客户端下线
else
{
UpdateOnline(client, false);
AddLog(0, client + "下线了");
CurrentClientlist.Remove(client);
break;
}
}
}
#endregion
字节数组转16进制字符串方法
#region 16进制字符串处理
///
/// 字节数组
/// 起始位置
/// 截取的数组长度
///
private string HexGetString(byte[] buffer, int start, int length)
{
string Result = string.Empty;
if (buffer != null && buffer.Length >= start + length)
{
//截取字节数组
byte\[\] res = new byte\[length\];
Array.Copy(buffer, start, res, 0, length);
string Hex = Encoding.Default.GetString(res, 0, res.Length);
// 01 03 0 40 0A
if (Hex.Contains(" "))
{
string\[\] str = Regex.Split(Hex, "\\\\s+", RegexOptions.IgnoreCase);
foreach (var item in str)
{
Result += "0x" + item + " ";
}
}
else
{
Result += "0x" + Hex;
}
}
else
{
Result = "Error";
}
return Result;
}
#endregion
更新在线列表方法,当客户端上线/下线的时候,服务器端的列表可以更新。
#region 在线列表更新
///
/// 客户端ip
/// 从列表中移除或者从列表中增加
private void UpdateOnline(string client, bool operate)
{
//是否是跨线程访问
if (!this.lst_Online.InvokeRequired)
{
if (operate)
{
this.lst_Online.Items.Add(client);
}
else
{
foreach (string item in this.lst_Online.Items)
{
if (item == client)
{
this.lst_Online.Items.Remove(item);
break;
}
}
}
}
else
{
Invoke(new Action(() =>
{
if (operate)
{
this.lst_Online.Items.Add(client);
}
else
{
foreach (string item in this.lst_Online.Items)
{
if (item == client)
{
this.lst_Online.Items.Remove(item);
break;
}
}
}
}));
}
}
#endregion
显示日志方法
/// <summary>
/// 显示日志的方法
/// </summary>
/// <param name="index">0,1,2分别表示显示日志信息的种类:正常,警告,错误</param>
/// <param name="info">日志信息</param>
private void AddLog(int index, string info)
{
if (!this.lst\_Rcv.InvokeRequired)
{
ListViewItem lst = new ListViewItem(" " + CurrentTime, index);
lst.SubItems.Add(info);
lst\_Rcv.Items.Insert(lst\_Rcv.Items.Count, lst);
}
else
{
Invoke(new Action(() =>
{
ListViewItem lst = new ListViewItem(" " + CurrentTime, index);
lst.SubItems.Add(info);
lst\_Rcv.Items.Insert(lst\_Rcv.Items.Count, lst);
}));
}
}
接收数据类型的枚举
public enum MessageType
{
ASCII,
UTF8,
Hex,
File,
JSON
}
服务器发送指定格式的消息数据给指定的客户端和群发客户端,这里首先要将发送的数据转换为你要发送的那种数据格式
//创建字典集合,键是ClientIp,值是SocketClient
private Dictionary
#region 发送ASCII
private void btn\_SendASCII\_Click(object sender, EventArgs e)
{
//选择要发送的数据的客户端
if (this.lst\_Online.SelectedItems.Count > 0)
{
AddLog(0, "发送内容:" + this.txt\_Send.Text.Trim());
byte\[\] send = Encoding.ASCII.GetBytes(this.txt\_Send.Text.Trim());
//创建最终发送的数组
byte\[\] sendMsg = new byte\[send.Length + 1\];
//整体拷贝数组,把send数组,从索引为1开始,拷贝send.Length个长度的字节数组给sendMsg
Array.Copy(send, 0, sendMsg, 1, send.Length);
//给首字节赋值,为了方便客户端识别该数据是什么类型的数据
sendMsg\[0\] = (byte)MessageType.ASCII;
foreach (var item in this.lst\_Online.SelectedItems)
{
//获取Socket对象
string client = item.ToString();
CurrentClientlist\[client\]?.Send(sendMsg);
}
this.txt\_Send.Clear();
}
else
{
MessageBox.Show("请选择你要发送的客户端对象!", "发送消息");
}
}
#endregion
#region 发送Hex
private void btn\_SendHex\_Click(object sender, EventArgs e)
{
if (this.lst\_Online.SelectedItems.Count > 0)
{
AddLog(0, "发送内容:" + this.txt\_Send.Text.Trim());
//默认的是Hex
byte\[\] send = Encoding.Default.GetBytes(this.txt\_Send.Text.Trim());
//创建最终发送的数组
byte\[\] sendMsg = new byte\[send.Length + 1\];
//整体拷贝数组,把send数组,从索引为1开始,拷贝send.Length个长度的字节数组给sendMsg
Array.Copy(send, 0, sendMsg, 1, send.Length);
//给首字节赋值
sendMsg\[0\] = (byte)MessageType.Hex;
foreach (var item in this.lst\_Online.SelectedItems)
{
//获取Socket对象
string client = item.ToString();
CurrentClientlist\[client\]?.Send(sendMsg);
}
this.txt\_Send.Clear();
}
else
{
MessageBox.Show("请选择你要发送的客户端对象!", "发送消息");
}
}
#endregion
#region 发送UTF8
private void btn\_SendUTF8\_Click(object sender, EventArgs e)
{
if (this.lst\_Online.SelectedItems.Count > 0)
{
AddLog(0, "发送内容:" + this.txt\_Send.Text.Trim());
byte\[\] send = Encoding.UTF8.GetBytes(this.txt\_Send.Text.Trim());
//创建最终发送的数组
byte\[\] sendMsg = new byte\[send.Length + 1\];
//整体拷贝数组
Array.Copy(send, 0, sendMsg, 1, send.Length);
//给首字节赋值
sendMsg\[0\] = (byte)MessageType.UTF8;
foreach (var item in this.lst\_Online.SelectedItems)
{
//获取Socket对象
string client = item.ToString();
CurrentClientlist\[client\]?.Send(sendMsg);
}
this.txt\_Send.Clear();
}
else
{
MessageBox.Show("请选择你要发送的客户端对象!", "发送消息");
}
}
#endregion
#region 发送文件
private void btn\_SendFile\_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(this.txt\_File.Text))
{
MessageBox.Show("请先选择你要发送的文件路径", "发送文件");
return;
}
else
{
if (this.lst\_Online.SelectedItems.Count > 0)
{
//分两次发送:第一次发送文件名,第二次发送文件内容
using (FileStream fs = new FileStream(this.txt\_File.Text, FileMode.Open))
{
//第一次发送文件名称
//获取文件名称
string filename = Path.GetFileName(this.txt\_File.Text);
//获取后缀名
string fileExtension = Path.GetExtension(this.txt\_File.Text);
string strMsg = "发送文件:" + filename + "." + fileExtension;
byte\[\] send1 = Encoding.UTF8.GetBytes(strMsg);
byte\[\] send1Msg = new byte\[send1.Length + 1\];
Array.Copy(send1, 0, send1Msg, 1, send1.Length);
send1Msg\[0\] = (byte)MessageType.UTF8;
foreach (var item in this.lst\_Online.SelectedItems)
{
//获取Socket对象
string client = item.ToString();
CurrentClientlist\[client\]?.Send(send1Msg);
}
//第二次发送文件内容
byte\[\] send2 = new byte\[1024 \* 1024 \* 10\];
//有效长度
int length = fs.Read(send2, 0, send2.Length);
byte\[\] send2Msg = new byte\[length + 1\];
Array.Copy(send2, 0, send2Msg, 1, length);
send2Msg\[0\] = (byte)MessageType.File;
foreach (var item in this.lst\_Online.SelectedItems)
{
//获取Socket对象
string client = item.ToString();
CurrentClientlist\[client\]?.Send(send2Msg);
}
this.txt\_File.Clear();
AddLog(0, strMsg);
}
}
else
{
MessageBox.Show("请选择你要发送的客户端对象!", "发送消息");
}
}
}
#endregion
#region 选择文件
private void btn\_SelectFile\_Click(object sender, EventArgs e)
{
OpenFileDialog ofd = new OpenFileDialog();
//设置默认的路径
ofd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
if (ofd.ShowDialog() == DialogResult.OK)
{
this.txt\_File.Text = ofd.FileName;
AddLog(0, "选择文件:" + this.txt\_File.Text);
}
}
#endregion
#region 发送JSON
private void btn_SendJSON_Click(object sender, EventArgs e)
{
if (this.lst_Online.SelectedItems.Count > 0)
{
//创建集合
List
{
new Student(){ StudentID=10001,StudentName="小明",ClassName="软件一班"},
new Student(){ StudentID=10002,StudentName="小红",ClassName="软件二班"},
new Student(){ StudentID=10003,StudentName="小花",ClassName="软件三班"},
};
//将对象转换为JSON字符串
string str = JSONHelper.EntityToJSON(stuList);
byte\[\] send = Encoding.Default.GetBytes(str);
byte\[\] sendMsg = new byte\[send.Length + 1\];
Array.Copy(send, 0, sendMsg, 1, send.Length);
sendMsg\[0\] = (byte)MessageType.JSON;
foreach (var item in this.lst\_Online.SelectedItems)
{
//获取Socket对象
string client = item.ToString();
CurrentClientlist\[client\]?.Send(sendMsg);
}
}
else
{
MessageBox.Show("请选择你要发送的客户端对象!", "发送消息");
}
}
#endregion
JsonHelper类
public class JSONHelper
{
/// <summary>
/// 实体对象转换成JSON字符串
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="x"></param>
/// <returns></returns>
public static string EntityToJSON<T>(T x)
{
string result = string.Empty;
try
{
result = JsonConvert.SerializeObject(x);
}
catch (Exception)
{
result = string.Empty;
}
return result;
}
/// <summary>
/// JSON字符串转换成实体类
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="json"></param>
/// <returns></returns>
public static T JSONToEntity<T>(string json)
{
T t = default(T);
try
{
t = (T)JsonConvert.DeserializeObject(json, typeof(T));
}
catch (Exception)
{
t = default(T);
}
return t;
}
}
客户端程序编写步骤:
第一步,调用socket()函数创建一个用于通信的套接字。
第二步,通过设置套接字地址结构,说明客户端与之通信的服务器的Ip地址和端口号。
第三步,调用connect()函数来建立与服务器的连接。
第四步,调用读写函数发送或者接收数据。
第五步,终止连接。
界面搭建:
连接服务器,同时客户端要判断服务器是否断开连接
#region 连接服务器
private void btn\_Connect\_Click(object sender, EventArgs e)
{
AddLog(0, "与服务器连接中");
// 第一步:调用socket()函数创建一个用于通信的套接字。
socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 第二步:通过设置套接字地址结构,说明客户端与之通信的服务器的IP地址和端口号。
IPEndPoint ipe = new IPEndPoint(IPAddress.Parse(this.txt\_IP.Text.Trim()), int.Parse(this.txt\_Port.Text.Trim()));
// 第三步:调用connect()函数来建立与服务器的连接。
try
{
socketClient.Connect(ipe);
}
catch (Exception ex)
{
AddLog(2, "连接服务器失败:" + ex.Message);
return;
}
//创建一个监听来自服务器消息的线程
Task.Run(new Action(() =>
{
CheckReceiveMsg();
}));
AddLog(0, "成功连接至服务器");
this.btn\_Connect.Enabled = false;
}
#endregion
#region 多线程接收来自服务器的数据
private void CheckReceiveMsg()
{
while (true)
{
// 创建一个缓冲区
byte\[\] buffer = new byte\[1024 \* 1024 \* 10\];
int length = -1;
// 第四步:调用读写函数发送或者接收数据。
try
{
length = socketClient.Receive(buffer);
}
catch (Exception)
{
// 服务器断线,可以添加服务器断线的日志
break;
}
if (length > 0)
{
string msg = string.Empty;
MessageType type = (MessageType)buffer\[0\];
switch (type)
{
case MessageType.ASCII:
//来自服务器的消息内容
msg = Encoding.ASCII.GetString(buffer, 1, length - 1);
AddLog(0, "服务器:" + msg);
break;
case MessageType.UTF8:
msg = Encoding.UTF8.GetString(buffer, 1, length - 1);
AddLog(0, "服务器:" + msg);
break;
case MessageType.Hex:
msg = HexGetString(buffer, 1, length - 1);
AddLog(0, "服务器:" + msg);
break;
case MessageType.File:
Invoke(new Action(() =>
{
SaveFileDialog sfd = new SaveFileDialog();
sfd.Filter = "txt files(\*.txt)|\*.txt|xls files(\*.xls)|\*.xls|xlsx files(\*.xlsx)|\*.xlsx|All files(\*.\*)|\*.\*";
if (sfd.ShowDialog() == DialogResult.OK)
{
string fileSavePath = sfd.FileName;
using (FileStream fs = new FileStream(fileSavePath, FileMode.Create))
{
fs.Write(buffer, 1, length - 1);
}
AddLog(0, "文件成功保存至" + fileSavePath);
}
}));
break;
case MessageType.JSON:
Invoke(new Action(() =>
{
string res = Encoding.Default.GetString(buffer, 1, length);
List<Student> StuList = JSONHelper.JSONToEntity<List<Student>>(res);
new FrmJSON(StuList).Show();
AddLog(0, "接收JSON数据:" + res);
}));
break;
default:
break;
}
}
else
{
// 如果length<=0,说明服务器断线了,可以添加服务器断线的日志
break;
}
}
}
#endregion
客服端发送消息给服务器,这里就只发送ASCLL,其余的数据格式和服务器端类似,有兴趣的可以下载源码查看。
#region 发送ASCII
private void btn\_SendASCII\_Click(object sender, EventArgs e)
{
AddLog(0, "发送内容:" + this.txt\_Send.Text.Trim());
byte\[\] send = Encoding.ASCII.GetBytes(this.txt\_Send.Text.Trim());
//创建最终发送的数组
byte\[\] sendMsg = new byte\[send.Length + 1\];
//整体拷贝数组
Array.Copy(send, 0, sendMsg, 1, send.Length);
//给首字节赋值
sendMsg\[0\] = (byte)MessageType.ASCII;
socketClient?.Send(sendMsg);
this.txt\_Send.Clear();
}
#endregion
客户端下线时,需要断开与服务器的连接
#region 窗体关闭
private void FrmTCPClient\_FormClosing(object sender, FormClosingEventArgs e)
{
//客户端下线的时候,关闭客户端套接字
socketClient?.Close();
}
#endregion
断开服务器
#region 断开服务器
private void btn\_DisConn\_Click(object sender, EventArgs e)
{
socketClient?.Close();
}
#endregion
源码下载链接:https://pan.baidu.com/s/1wIXLC0AlGyM1VoYeYOKMpg
_提取码:8qjn
_
,把send数组,从索引为1开始,拷贝send.Length个长度的字节数组给sendMsg
手机扫一扫
移动阅读更方便
你可能感兴趣的文章