C# TCP/IP网络数据传输及实现
一、概念简述
1、什么是OSI 和TCP/IP
??国际标准化组织(ISO)在1985年研究的网络互连模型,定义了OSI(Open System Interconnect),即开放式系统互联。 也叫OSI参考模型,OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)。这个框架定义了理论的标准模型,也是通讯学习基础的必修内容。
??实际网络中TCP/IP协议中使用“TCP/IP五层模型”,与其各层有各自的协议, 来保证能互联网中正常通讯。下面用表格简述了模型的对应关系。
2、什么是套接字Socket
??百度百科的解释:所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。 ??简单理解就是 Socket就是应用层与TCP/IP协议族通信的中间软件抽象层。将传输层封装后便与应用层更方便的调用。
3、TCP 和 UDP
??TCP(Transmission Control Protocol) —— 传输控制协议 ??UDP(User Datagram Protocol) —— 用户数据报协议 ??从协议层我们可以看出TCP和UDP协议已经处在传输层,也就是说该层次已经可以完成数据的传输。他们的两者的主要差异如下面表格:
属性 | UDP | TCP |
---|
连接方式 | 非连接 | 面向连接 | 可靠性 | 不可靠传输 | 可靠传输 | 连接对象 | 一对一、一对多、多对一、多对多 | 只能一对一 | 流量拥塞 | 无 | 有 | 数据类型 | 面向报文 | 面向字节流 | 适用场景 | 适用于实时数据传输(视频、电话等) | 适用于可靠准确的数据传输(文件、邮件) |
??TCP 是面向连接的传输协议,建立连接时要经过三次握手,断开连接时要经过四次握手,中间传输数据时也要回复 ACK 包确认,多种机制保证了数据能够正确到达,不会丢失或出错。 ??UDP 是非连接的传输协议,没有建立连接和断开连接的过程,它只是简单地把数据丢到网络中,也不需要 ACK 包确认。 ??与 UDP 相比,TCP 有较为复杂的通讯交互和流控制也只能是1对1的方式,但是这保证了数据传输的正确可靠性。而TCP 的速度是无法超越 UDP,两组协议各有千秋,应用层按照自己的需求选择使用他们,发挥他俩各自的优势。
4、IP 、MAC、PORT
(1) IP地址
??IP地址是 Internet Protocol Address 的缩写,译为“网际协议地址”。 I??P协议是为计算机网络相互连接进行通信而设计的协议。它处在TCP/IP 协议栈模型的网络层。在因特网中,它是能使连接到网上的所有计算机网络实现相互通信的一套规则,规定了计算机在因特网上进行通信时应当遵守的规则。任何厂家生产的计算机系统,只要遵守IP协议就可以与因特网互连互通。 ??一台计算机可以拥有一个独立的 IP 地址,一个局域网也可以拥有一个独立的 IP 地址(如同只有一台计算机)。对于目前广泛使用 IPv4 地址,它的资源是非常有限的。在因特网上进行通信时,必须要知道对方的 IP 地址。他就好像是对方的名牌号码,如同收件地址一样。实际上数据包中已经附带了 IP 地址,把数据包发送给路由器以后,路由器会根据 IP 地址找到对方的地里位置,完成一次数据的传递。 ??IP地址类别分公有地址和私有地址。公有地址(Public address)由Inter NIC(Internet Network Information Center因特网信息中心)负责。这些IP地址分配给注册并向Inter NIC提出申请的组织机构。通过它直接访问因特网。私有地址(Private address)属于非注册地址,专门为组织机构内部使用。 以下列出留用的内部私有地址: ??A类 10.0.0.0–10.255.255.255 ??B类 172.16.0.0–172.31.255.255 ??C类 192.168.0.0–192.168.255.255 ??另外,还有另一类特殊的D类地址也叫组播地址,范围从224.0.0.0到239.255.255.255。 ??IP地址通过子网掩码(NetMask)来区分自己所在的网段。
(2) MAC地址
??MAC 地址是 Media Access Control Address 的缩写,直译为“媒体访问控制地址”,也称为局域网地址(LAN Address),以太网地址(Ethernet Address)或物理地址(Physical Address)。它是一个用来确认网络设备位置的位址。 通讯模型中网络层负责IP地址, 据链路层则负责MAC位址 。 MAC地址用于在网络中唯一标示一个网卡,一台设备如果存在多个网卡,则每个网卡都会有一个唯一的MAC地址。网络数据包中除了会附带对方的 IP 地址,ARP协议会将数据包达到局域网以后,路由器/交换机会根据数据包中的 MAC 地址找到对应的计算机,然后把数据包转交给它,这样就完成了数据的传递。IPv4地址为32位二进制数,MAC地址则是为48位二进制数表示,一般用16进制显示。
(3) Port端口号
?? IP 地址和 MAC 地址,虽然可以找到目标计算机,但仍然不能进行通信。因为一台计算机可以同时提供多种网络服务,例如 Web 服务(网站)、FTP 服务(文件传输服务)、SMTP 服务(邮箱服务)等。为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web 服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。 ??端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门或者一个通道,数据通过的这个门或者通道的不同的编号,就是端口号,程序是可以自己定义的。个IP地址的端口通过16bit进行编号,最多可以有65536个端口 。端口号只有整数,范围是从0 到65535。
二、 UDP上位机的实现
1、 准备
?? 当我们使用网线连接网络有较好的通信网络时,用C# 来实现UDP通信来做上位机的应用是一个不错的选择,它既能支持多路通讯和也有速度快的优势。 ?? 首先,要安装好C#的编译编辑平台,我这里是使用的VS2019。安装过程不再描述可以自己搜索; 其次,建立好自己的C# .NETframework工程;然后,就可以开始修改添加自己的代码了。 ?? 码代码前,强调!强调!强调! 务必要理解前面一章描述的基本概念,以及按照工程需求建立起自己想要的逻辑实现框架流程,完全不懂可以在网上找一些实现流程和基础代码来解读。我的实现是直接加载Form框架load中,这也正符合我实现的需求。将需要的TCPIP的socket实现或其它功能块写在独立文件的类中,这里注意哟,要包含到同一个空间命名中。
2、 Form主流程实现
上代码 !先描述主流程
TCPSocket _TcpSocket = new TCPSocket();
FileOperation _FileOperation = new FileOperation();
private void Form_Moniter_Load(object sender, EventArgs e)
{
if (CheckLocalIpAddressExist() == false)
{
return;
}
this.InitTcpSocket();
if (_TcpSocket.UdpOpen() == false)
{
return;
}
timer_system.Start();
this.InitControlTool();
}
private void InitTcpSocket()
{
string GetFileConnectType = _FileOperation._iniConnectType;
int GetFileConnectType_int = 0;
if (GetFileConnectType == "")
{
goto TypeErrEND;
}
if (!Utils.IsNumeric(GetFileConnectType))
{
goto TypeErrEND;
}
GetFileConnectType_int = Convert.ToInt32(GetFileConnectType);
switch (GetFileConnectType_int )
{
#if false
case 0:
_TcpSocket._tcpType = TCPType.TCPServer;
return;
case 1:
_TcpSocket._tcpType = TCPType.TCPClient;
return;
#endif
case 2:
_TcpSocket._tcpType = TCPType.UDP;
_TcpSocket._localIP = GetIniFileUdpLocalIp();
_TcpSocket._localPort = GetIniFileUdpLocalPort();
_TcpSocket._targetIP = GetIniFileUdpTargetIp();
_TcpSocket._targetPort = GetIniFileUdpTargetPort();
return;
default:
break;
}
TypeErrEND:
_TcpSocket._tcpType = TCPType.UDP;
_FileOperation._iniConnectType = ((int)TCPType.UDP).ToString();
_TcpSocket._localIP = GetIniFileUdpLocalIp();
_TcpSocket._localPort = GetIniFileUdpLocalPort();
_TcpSocket._targetIP = GetIniFileUdpTargetIp();
_TcpSocket._targetPort = GetIniFileUdpTargetPort();
}
?? 上面的Form_Moniter_Load()窗体加载函数,已经包含了开启UDP开启的整个流程 。 虽然简单,还是给个流程图吧:
Created with Rapha?l 2.3.0
Form_Moniter_Load()开始
PC是否有正确的IP地址?
初始化TCPSocket
确设置打开UDP线程?
初始化定时器
初始化控件状态
Form_Moniter_Load()结束
yes
no
yes
no
?? 其中InitTcpSocket()则是初始化了我们需要的参数,我是从配置文件读取的,也可以采用自己定义的方法,如果没有正确参数,则写入默认值,因此也不需要返回正确错误的结果。接着Open UDP就可以开始了,正确打开后就完成其它的界面初始化的工作了。流程也很简单。不做过多讲解。
3、 Socket类的实现
?? Socket的实现的代码我们把它分为几部分函数:参数部分、UDP的打开、数据发送、数据接收以及关闭。
(1) 参数部分
public enum TCPType
{
TCPServer,
TCPClient,
UDP
}
public event EventHandler BufferReceChange;
public event EventHandler ConnStateChange;
private void OnBufferReceChange(EventArgs eventArgs)
{
this.BufferReceChange?.Invoke(this, eventArgs);
}
private void OnConnStateChange(EventArgs eventArgs)
{
this.ConnStateChange?.Invoke(this, eventArgs);
}
#endregion
#region ====> UDP 各类参数
private bool connState = false;
public bool _connState
{
get { return connState; }
set
{
this.OnConnStateChange(new EventArgs());
connState = value;
}
}
private string targetIp = "255.255.255.255";
public string _targetIP
{
get { return targetIp; }
set { targetIp = value; }
}
private int targetPort = 8000;
public int _targetPort
{
get { return targetPort; }
set { targetPort = value; }
}
private string localIP_Single= Utils.GetIPAddressSingle();
public string _localIP_Single
{
get { return localIP_Single; }
}
private string localIP_All = Utils.GetIPAddressAll();
public string _localIP_ALL
{
get { return localIP_All; }
}
private string currentIP;
public string _currentIP
{
get { return currentIP; }
set { currentIP = value; }
}
private int currentPort;
public int _currentPort
{
get { return currentPort; }
set { currentPort = value; }
}
private string localIP;
public string _localIP
{
get { return localIP; }
set { localIP = value; }
}
private int localPort;
public int _localPort
{
get { return localPort; }
set { localPort = value; }
}
private byte[] udpRx;
public byte[] _udpReceiveData
{
get { return udpRx; }
set { udpRx = value; }
}
private TCPType tcpType = TCPType.UDP;
public TCPType _tcpType
{
get { return tcpType; }
set { tcpType = value; }
}
Thread UdpReceiveDataThread = null;
private UdpClient udp = new UdpClient();
private IPEndPoint localIpep = null;
??参数部分首先定义了委托的事件,程序指定了数据不为空就跳转到委托函数中运行。为UDP接收数据传递即时消息给到上层使用,就好像中断消息处理的意思。 ??连接状态、TCP类型等等各类参数,通过GET{}和SET{}方法来方便供外部赋值和获取。 ??最后,实例化线程和UDP服务。
(2) UDP的数据打开、接收、发送和关闭
public bool UdpOpen()
{
try
{
UdpCLose();
localIpep = new IPEndPoint(IPAddress.Parse(localIP), localPort);
udp = new UdpClient(localIpep);
UdpReceiveDataThread = new Thread(new ThreadStart(UdpRxThread));
UdpReceiveDataThread.IsBackground = true;
UdpReceiveDataThread.Name = "UDP接收线程";
UdpReceiveDataThread.Start();
connState = true;
return true;
}
catch (Exception ex)
{
_connState = false;
if (MessageBox.Show("连接失败:\n Msg:" + ex.Message + "\nSource:" + ex.Source + "\n[OK]继续尝试,或[Cancel]退出程序?", "异常提示/询问",
MessageBoxButtons.OKCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.OK)
{
UdpOpen();
}
return false;
}
finally
{
this.OnConnStateChange(new EventArgs());
}
}
public bool UdpSend(byte[] buffer)
{
try
{
udp.Send(buffer, buffer.Length, _targetIP, _targetPort);
return true;
}
catch (Exception ex)
{
_connState = false;
ExceptionContent = ex.Message + "\n" + ex.ToString();
this.OnConnStateChange(new EventArgs());
return false;
}
}
public void UdpRxThread()
{
try
{
while (true)
{
byte[] receiveBytes = udp.Receive(ref localIpep);
currentIP = localIpep.Address.ToString();
currentPort = localIpep.Port;
udpReceiveData = new byte[receiveBytes.Length];
Buffer.BlockCopy(receiveBytes, 0, _udpReceiveData, 0, receiveBytes.Length);
this.OnBufferReceChange(new EventArgs());
}
}
catch (Exception ex)
{
_connState = false;
ExceptionContent = ex.Message + "\n" + ex.ToString();
this.OnConnStateChange(new EventArgs());
}
}
private void UdpCLose()
{
try
{
udp.Close();
}
catch (Exception ex)
{
ExceptionContent = ex.Message + "\n" + ex.ToString();
return;
}
}
??打开UdpOpen方法是这里最为关键的一环,首先要知道UDP也是有server(服务器)端和client(客户)端,而上位机这是client端,因此他也是首先发起数据的一方,服务器端的操作需要绑定(bind)设备IP和端口,客户端则不需要,因此这里IP地址可以使用自己的IP也可以使用0.0.0.0作为IP地址。在线程开始前,localIpep 指定了自己的IP以及端口。需要注意的是,网线连接状态或者UDP服务是否被占用的问题异常处理,以及及时要更新自己的连接状态。 ??发送数据UdpSend()太简单,将udp.Send(buffer, buffer.Length, _targetIP, _targetPort)函数的几个参数代入就能实现将buffer中的数据发送出去。 ??接收的线程方法UdpRxThread最关键的就是udp.Receive(ref localIpep),这个函数将监听接收到的数据,否者将会一直等待,有数据时,将其拷贝出来即可,通过委托消息传递给上层显示。 ??退出UdpCLose()功能就是关闭Open状态。
三、TCP上位机实现
(待更新)
|