C#实现套接字数据发送
系统 Windows10
IDE VS2019
文章目录
- C#实现套接字数据发送
- 一、什么是UDP、套接字
- 1. UDP
- 2. Socket(套接字)
- 二、C#实现HelloWorld
- 三、C#控制台程序,利用UDP套接字实现消息的发送
- 1. 发送数据
- 2. WireShark抓包
- 四、C#窗口程序,利用TCP套接字实现消息的发送
- 1. 发送数据
- 2. WireShark抓包
- 五、C#窗口程序,实现端口扫描器
- 1. 单线程扫描
- 2. 多线程扫描
- 六、总结
- 七、参考文章
一、什么是UDP、套接字
1. UDP
用户数据包协议(User Datagram Protocol),简称UDP,是Internet协议集支持的一个无连接的传输协议。UDP为应用程序提供了一种无需建立连接就可以发送封装IP数据包的方法。
Internet的传输层有两个主要协议,护卫补充。无需建立连接的是UDP,它除了给应用程序发送数据包功能并允许它们在所需的层次上架构自己的协议之外,几乎没有做什么特别的事情。面向连接的是 TCP,该协议几乎做了所有的事情。
特点: 无连接、不可靠、快速传输
UDP与TCP一样用于处理数据包,在OSI(Open System Interconnection,开放式系统互联)模型中,两者都位于传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达。UDP用来支持那些需要在计算机之间传输数据的网络应用,比如网络视频会议系统在内的众多客户/服务器模式的网络应用都需要使用UDP协议。
许多应用只支持UDP,比如:多媒体数据流,不产生任何额外的数据,及时知道有破坏的包也不进行重发;音频和多媒体应用,强调传输性能而不是完整性。这些都以UDP作为最好的发送数据包的方法。
2. Socket(套接字)
套接字对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
它上联应用进程,下联网络栈协议。是应用程序通过网络协议进行通信的接口,与网络协议栈进行交互的接口。
套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。可以将套接字看作不同主机间的进程进行双间通信的端点,它构成了单个主机及整个网络间的编程界面。
二、C#实现HelloWorld
-
打开VS2019,点击创建新项目,选择C#控制台应用,点击下一步,确定项目名称和存储路径后再次点击下一步创建项目
-
可以看见VS2019的C#控制台应用默认就是输出HelloWorld的代码,所以我们这里加一个for循环即可
-
点击运行,结果如下:
三、C#控制台程序,利用UDP套接字实现消息的发送
1. 发送数据
这是一个简单的UDP通信实例。
目前最普遍的服务模式是C/S模式,所以需要一个客户端client和一个服务器端Server,来实现通信,所以需要两台主机来分别运行客户端和服务器端(你也可以使用同一台电脑同时运行两个程序来模拟)
-
重新创建一个C#控制台项目作为服务器接收端
-
输入以下代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Client
{
class Program
{
static void Main(string[] args)
{
//提示信息
Console.WriteLine("按下任意按键开始发送...");
Console.ReadKey();
int m;
//做好链接准备
UdpClient client = new UdpClient(); //实例一个端口
IPAddress remoteIP = IPAddress.Parse("10.60.118.11"); //假设发送给这个IP
int remotePort = 11000; //设置端口号
IPEndPoint remotePoint = new IPEndPoint(remoteIP, remotePort); //实例化一个远程端点
for (int i = 0; i < 50; i++)
{
//要发送的数据:第n行:hello cqjtu!重交物联2018级
string sendString = null;
sendString += "第";
m = i + 1;
sendString += m.ToString();
sendString += "行:hello cqjtu!重交物联2019级";
//定义发送的字节数组
//将字符串转化为字节并存储到字节数组中
byte[] sendData = null;
sendData = Encoding.Default.GetBytes(sendString);
client.Send(sendData, sendData.Length, remotePoint);//将数据发送到远程端点
}
client.Close();//关闭连接
//提示信息
Console.WriteLine("");
Console.WriteLine("数据发送成功,按任意键退出...");
System.Console.ReadKey();
}
}
}
- 显示提示信息,表示正等待操作
- 做好链接准备:设置IP、端口号等
- for循环发送数据
- 关闭端口
- 显示提示信息,等待用户确认退出
-
在另一台电脑上创建一个C#作为客户端给服务器端发送消息
-
输入以下代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Server
{
class Program
{
static void Main(string[] args)
{
int result;
string str = "第50行:hello cqjtu!重交物联2018级";
UdpClient client = new UdpClient(11000);
string receiveString = null;
byte[] receiveData = null;
//实例化一个远程端点,IP和端口可以随意指定,等调用client.Receive(ref remotePoint)时会将该端点改成真正发送端端点
IPEndPoint remotePoint = new IPEndPoint(IPAddress.Any, 0);
Console.WriteLine("正在准备接收数据...");
while (true)
{
receiveData = client.Receive(ref remotePoint);//接收数据
receiveString = Encoding.Default.GetString(receiveData);
Console.WriteLine(receiveString);
result = String.Compare(receiveString, str);
if (result == 0)
{
break;
}
}
client.Close();//关闭连接
Console.WriteLine("");
Console.WriteLine("数据接收完毕,按任意键退出...");
System.Console.ReadKey();
}
}
}
-
两台电脑同时运行,再在客户端上按下任意键触发发送程序
- 做好连接准备,并设置结束标志
- 循环接收数据
- 关闭连接
- 显示提示信息,等待用户确定退出
-
运行结果如下:
2. WireShark抓包
-
打开wireshark,点击正在使用的网络并暂停抓包,防止数据太多影响查找
-
重新启动客户端,向服务器端发送数据,发送之前启动抓包
-
在过滤器输入udp进行一个简单过滤
-
找到源地址和目的地址符合的包,点击可以看到不难发现的hello cqjtu,证明这就是我们要抓的包
-
点击
Ethernet II
,这就是数据链路层的帧的头部,相应的蓝色部分的十六进制对应帧头部数据
-
一个帧的头部的主要结构是:
-
目的地址:数据接收方,这里代表10.60.118.11
-
源地址:数据发送方,这里代表10.60.118.10
-
数据类型:数据类型(Type):分为 IP(0800) 包和 ARP(0806) 包,这个包是 0800 ,所以是 IP 包。
-
数据:后面三排,上层(网络层)传下来的一个 IP 包,数据部分又分为数据部分和填充部分,当这个 IP 包的长度小于 46 个字节,比如数据部分长度为 30 ,那么填充部分(垃圾信息)就是 16 个字节,保证这个帧的总长度最短为 64 个字节(其原因可自行百度),如果数据部分长度为50,那么填充部分就为 0 个字节,最后,数据(数据部分 + 填充部分)的长度最长为 1500 个字节,不能过大,所以帧的总长度最长为 1518 字节,抓取的所有包,都不会超过这个长度。
-
校验码:帧的末尾有 4 字节的校验码,但是抓包软件会自动舍去。
-
-
再来分析一下网络层的 IP 包:
-
Version(版本号):分为 IPv4 和 IPv6 现在普遍都用的 IPv4 ,所以值为 4 ,1 个字节;
-
HLen(ip报头长度):32位字的报头长度(HLEN);
-
TOS(级别):服务类型描述数据报将如何被处理,比如优先发送等,大多数都是默认为 0 ;
-
Datagram Total Length(总长度):包括报头和数据的数据包长度;
-
identifier(标识):唯一的 IP 数据包值;
-
Flags(标志):说明是否有数据被分段,我是一条一条的发送多个数据包,每个包的数据很小,没有被分段,所以这里数值为 0 。
-
Fragmentation Offset(分段偏移):如果数据包在装人帧时太大,则需要进行分段和重组,这里没有分段,所以偏移量为 0 ;
-
TTL(存活期):存活期是在数据包产生时建立在其内部的一个设置,如果这个数据包在这个TTL到期时仍没有到达它要去的目的地,那么它将被丢弃,这个设置将防止IP包在寻找目的地的时候在网络中不断循环,每经过一个路由器,它的值就减一,这里它的值为 128 ,也就是 128 个生存期;
-
Protocol(协议):上层协议的端口( TCP 是端口 6;UDP 是端口 17) ,同样也支持网络层协议,如ARP和ICMP,这里值为 17 ,也就是 UDP 协议;
-
Header Checksum(校验码):只针对报头的循环冗余校验(CRC);
-
Source Address(源地址):消息发送者的 ip 地址,这里是10.60.191.19;
-
Destination Address(目的地址):消息接收者的 ip 地址,这里是10.60.202.32;
-
ip包头的第六行:用于网络检测、调试、安全以及更多的内容,不过大多数情况都是默认为 0 的;
最后来看一下数据包:
可以很显然看到,长度为 34 字节,对应的十六进制就是蓝色的区域了,这就是我们要发送的数据:第n行:hello cqjtu!重交物联2019级。
到此,一个 UDP 包就分析完了。
-
-
四、C#窗口程序,利用TCP套接字实现消息的发送
1. 发送数据
-
创建一个C#窗体程序,界面设置如下
-
代码如下:
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.Tasks; using System.Windows.Forms; namespace WinFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void textBox1_TextChanged(object sender, EventArgs e) { } private void button1_Click(object sender, EventArgs e) { try { /* * 显示当前时间 */ string str = "The current time: "; str += DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); textBox2.AppendText(str + Environment.NewLine); /* * 做好连接准备 */ int port = 2000; string host = "10.60.118.10";//我室友的IP地址 IPAddress ip = IPAddress.Parse(host); IPEndPoint ipe = new IPEndPoint(ip, port);//把ip和端口转化为IPEndPoint实例 Socket c = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//创建一个Socket /* * 开始连接 */ str = "Connect to server..."; textBox2.AppendText(str + Environment.NewLine); c.Connect(ipe);//连接到服务器 /* *发送消息 */ string sendStr = textBox1.Text; str = "The message content: " + sendStr; textBox2.AppendText(str + Environment.NewLine); byte[] bs = Encoding.UTF8.GetBytes(sendStr); str = "Send the message to the server..."; textBox2.AppendText(str + Environment.NewLine); c.Send(bs, bs.Length, 0);//发送信息 /* * 接收服务器端的反馈信息 */ string recvStr = ""; byte[] recvBytes = new byte[1024]; int bytes; bytes = c.Receive(recvBytes, recvBytes.Length, 0);//从服务器端接受返回信息 recvStr += Encoding.UTF8.GetString(recvBytes, 0, bytes); str = "The server feedback: " + recvStr;//显示服务器返回信息 textBox2.AppendText(str + Environment.NewLine); /* * 关闭socket */ c.Close(); } catch (ArgumentNullException f) { string str = "ArgumentNullException: " + f.ToString(); textBox2.AppendText(str + Environment.NewLine); } catch (SocketException f) { string str = "ArgumentNullException: " + f.ToString(); textBox2.AppendText(str + Environment.NewLine); } textBox2.AppendText("" + Environment.NewLine); textBox1.Text = ""; } } }
-
创建一个服务器端代码
-
代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace ConsoleApp2 { class Program { static void Main(string[] args) { /* * 做好连接准备 */ int i = 0; int port = 2000; string host = "10.60.118.10"; IPAddress ip = IPAddress.Parse(host); IPEndPoint ipe = new IPEndPoint(ip, port); Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//创建一个Socket类 s.Bind(ipe);//绑定2000端口 /* * 循环监听并处理消息 */ while (true) { i++; try { Console.Write("Perform operations {0} :", i); Console.WriteLine("\t-----------------------------------------------"); s.Listen(0);//开始监听 Console.WriteLine("1. Wait for connect..."); /* * 实例一个新的socket端口 */ Socket temp = s.Accept();//为新建连接创建新的Socket。 Console.WriteLine("2. Get a connect"); /* * 接收客户端发的消息并做解码处理 */ string recvStr = ""; byte[] recvBytes = new byte[1024]; int bytes; bytes = temp.Receive(recvBytes, recvBytes.Length, 0);//从客户端接受信息 recvStr += Encoding.UTF8.GetString(recvBytes, 0, bytes); Console.WriteLine("3. Server Get Message:{0}", recvStr);//把客户端传来的信息显示出来 /* * 返回给客户端连接成功的消息 */ string sendStr = "Ok!Client send message sucessful!"; byte[] bs = Encoding.UTF8.GetBytes(sendStr); temp.Send(bs, bs.Length, 0);//返回客户端成功信息 /* * 关闭端口 */ temp.Close(); Console.WriteLine("4. Completed..."); Console.WriteLine("-----------------------------------------------------------------------"); Console.WriteLine(""); //s.Close();//关闭socket(由于再死循环中,所以不用写,但如果是单个接收,实例socket并完成任务后需关闭) } catch (ArgumentNullException e) { Console.WriteLine("ArgumentNullException: {0}", e); } catch (SocketException e) { Console.WriteLine("SocketException: {0}", e); } } } } }
-
运行效果如下:
2. WireShark抓包
-
和第一次抓包一样的步骤
-
下面就是我们发送的信息
-
TCP:
第三行:Internet Protocl Version 4:IPv4协议
第四行:Transmission Control Protocol:TCP协议 -
TCP不同于UDP,第四行是TCP协议
-
这个 ip 包头也是 45 00 开头,对比一下第二部分对 ip 包的分析,这个 ip 包头也是 45 00 开头,所以一个 ip 包的前两个字节一定是 45 00 。
-
生存期也是 128 ,一个包的生存期基本上都是默认的数值 128 ,而这里的协议号是 6 ,这就是 TCP 的协议号,第二部分的协议号是 17 ,是 UDP 的。
其余的数据和第二部分抓取的数据包差不多了,这里不再赘述了。
五、C#窗口程序,实现端口扫描器
1. 单线程扫描
创建一个新的窗体项目,设置以下界面:
代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsApp2
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//主机地址
private string hostAddress;
//起始端口
private int start;
//终止端口
private int end;
//端口号
private int port;
//定义线程对象
private Thread scanThread;
private void label1_Click(object sender, EventArgs e)
{
}
private void label2_Click(object sender, EventArgs e)
{
}
private void button1_Click(object sender, EventArgs e)
{
try
{
//初始化
textBox4.Clear();
label4.Text = "0%";
//获取ip地址和始末端口号
hostAddress = textBox1.Text;
start = Int32.Parse(textBox2.Text);
end = Int32.Parse(textBox3.Text);
if (decideAddress())
{
//让输入的textbox只读,无法改变
textBox1.ReadOnly = true;
textBox2.ReadOnly = true;
textBox3.ReadOnly = true;
//设置进度条的范围
progressBar1.Minimum = start;
progressBar1.Maximum = end;
//显示框显示
textBox4.AppendText("端口扫描器 v1.0.0" + Environment.NewLine + Environment.NewLine);
//调用端口扫描函数
PortScan();
}
else
{
//若端口号不合理,弹窗报错
MessageBox.Show("输入错误,端口范围为[0-65536]!");
}
}
catch
{
//若输入的端口号为非整型,则弹窗报错
MessageBox.Show("输入错误,端口范围为[0-65536]!");
}
}
private bool decideAddress()
{
//判断端口号是否合理
if ((start >= 0 && start <= 65536) && (end >= 0 && end <= 65536) && (start <= end))
return true;
else
return false;
}
private void PortScan()
{
double x;
string xian;
//显示扫描状态
textBox4.AppendText("开始扫描...(可能需要请您等待几分钟)" + Environment.NewLine + Environment.NewLine);
//循环抛出线程扫描端口
for (int i = start; i <= end; i++)
{
x = (double)(i - start + 1) / (end - start + 1);
xian = x.ToString("0%");
port = i;
//调用端口i的扫描操作
Scan();
//进度条值改变
label4.Text = xian;
label4.Refresh();
progressBar1.Value = i;
}
textBox4.AppendText(Environment.NewLine + "扫描结束!" + Environment.NewLine);
//输入框textbox只读属性取消
textBox1.ReadOnly = false;
textBox2.ReadOnly = false;
textBox3.ReadOnly = false;
}
private void Scan()
{
int portnow = port;
//创建TcpClient对象,TcpClient用于为TCP网络服务提供客户端连接
TcpClient objTCP = null;
try
{
//用于TcpClient对象扫描端口
objTCP = new TcpClient(hostAddress, portnow);
//扫描到则显示到显示框
textBox4.AppendText("端口 " + port + " 开放!" + Environment.NewLine);
}
catch
{
//未扫描到,则会抛出错误
}
}
}
}
运行效果如图所示:
单线程扫描比较慢,所以这里我们只设置了10个端口(连这样都花了一分钟)
2. 多线程扫描
窗口布局不变,在原来代码的基础上更改多线程
代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsApp2
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//主机地址
private string hostAddress;
//起始端口
private int start;
//终止端口
private int end;
//端口号
private int port;
//定义线程对象
private Thread scanThread;
//定义端口状态数据(开放则为true,否则为false)
private bool[] done = new bool[65526];
private bool OK;
private void label1_Click(object sender, EventArgs e)
{
}
private void label2_Click(object sender, EventArgs e)
{
}
private void button1_Click(object sender, EventArgs e)
{
try
{
//初始化
textBox4.Clear();
label4.Text = "0%";
//获取ip地址和始末端口号
hostAddress = textBox1.Text;
start = Int32.Parse(textBox2.Text);
end = Int32.Parse(textBox3.Text);
if (decideAddress())
{
//让输入的textbox只读,无法改变
textBox1.ReadOnly = true;
textBox2.ReadOnly = true;
textBox3.ReadOnly = true;
//设置进度条的范围
progressBar1.Minimum = start;
progressBar1.Maximum = end;
//显示框显示
textBox4.AppendText("端口扫描器 v1.0.0" + Environment.NewLine + Environment.NewLine);
//调用端口扫描函数
PortScan();
}
else
{
//若端口号不合理,弹窗报错
MessageBox.Show("输入错误,端口范围为[0-65536]!");
}
}
catch
{
//若输入的端口号为非整型,则弹窗报错
MessageBox.Show("输入错误,端口范围为[0-65536]!");
}
}
private bool decideAddress()
{
//判断端口号是否合理
if ((start >= 0 && start <= 65536) && (end >= 0 && end <= 65536) && (start <= end))
return true;
else
return false;
}
private void PortScan()
{
double x;
string xian;
//显示扫描状态
textBox4.AppendText("开始扫描...(可能需要请您等待几分钟)" + Environment.NewLine + Environment.NewLine);
//循环抛出线程扫描端口
for (int i = start; i <= end; i++)
{
x = (double)(i - start + 1) / (end - start + 1);
xian = x.ToString("0%");
port = i;
//使用该端口的扫描线程
scanThread = new Thread(new ThreadStart(Scan));
scanThread.Start();
//使线程睡眠
System.Threading.Thread.Sleep(100);
//进度条值改变
label4.Text = xian;
label4.Refresh();
progressBar1.Value = i;
}
while (!OK)
{
OK = true;
for (int i = start; i <= end; i++)
{
if (!done[i])
{
OK = false;
break;
}
}
System.Threading.Thread.Sleep(1000);
}
textBox4.AppendText(Environment.NewLine + "扫描结束!" + Environment.NewLine);
textBox1.ReadOnly = false;
textBox2.ReadOnly = false;
textBox3.ReadOnly = false;
}
private void Scan()
{
int portnow = port;
//创建线程变量
Thread Threadnow = scanThread;
//扫描端口,成功则写入信息
done[portnow] = true;
//创建TcpClient对象,TcpClient用于为TCP网络服务提供客户端连接
TcpClient objTCP = null;
try
{
//用于TcpClient对象扫描端口
objTCP = new TcpClient(hostAddress, portnow);
//扫描到则显示到显示框
textBox4.AppendText("端口 " + port + " 开放!" + Environment.NewLine);
}
catch
{
//未扫描到,则会抛出错误
}
}
}
}
运行效果如下:
我们可以看到扫描范围即使扩大了10倍但扫描速度也比之前快乐不少,进度条的刷新也很流畅,多线程工作明显节约了不少时间。
六、总结
UDP/TCP 套接字进行网络通信,这都是基于 C/S 模式,一个客户端,一个服务器端,在使用套接字的时候,端口号、IP地址是必不可缺的,缺一不可。
同时认识到了多线程工作的优越性,可以大大节省时间复杂度,成倍提升程序运行速度,因为多线程就是多个单线程同时进行,自然要快很多。
七、参考文章
可乐有点好喝: C#使用TCP/UDP协议通信并用Wireshark抓包分析数据
可乐有点好喝: C#单线程和多线程的应用比较:端口扫描器