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 的缩写,译为“网际协议地址”。
IP协议是为计算机网络相互连接进行通信而设计的协议。它处在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主流程实现
上代码 !先描述主Form流程
//自定义Socket类实例化
TCPSocket _TcpSocket = new TCPSocket();
//文件操作FileOperation类实例化
FileOperation _FileOperation = new FileOperation();
private void Form_Moniter_Load(object sender, EventArgs e)
{
if (CheckLocalIpAddressExist() == false)//检查本地IP
{
return;
}
//初始化TCPSocket
this.InitTcpSocket();
//设置打开UDP线程
if (_TcpSocket.UdpOpen() == false)
{
// 关闭程序
return;
}
//启动显示时间定时器
timer_system.Start();
//初始化控件
this.InitControlTool();
}
/// <summary>
/// 初始化设置网络连接类型
/// </summary>
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;//数字都为默认2
_FileOperation._iniConnectType = ((int)TCPType.UDP).ToString();//UDP写入文件
//读取本地IP和端口以及目标IP地址和端口
_TcpSocket._localIP = GetIniFileUdpLocalIp();
_TcpSocket._localPort = GetIniFileUdpLocalPort();
_TcpSocket._targetIP = GetIniFileUdpTargetIp();
_TcpSocket._targetPort = GetIniFileUdpTargetPort();
}
上面的Form_Moniter_Load()窗体加载函数,已经包含了开启UDP开启的整个流程 。 虽然简单,还是给个流程图吧:
其中InitTcpSocket()则是初始化了我们需要的参数,我是从配置文件读取的,也可以采用自己定义的方法,如果没有正确参数,则写入默认值,因此也不需要返回正确错误的结果。接着Open UDP就可以开始了,正确打开后就完成其它的界面初始化的工作了。流程也很简单。不做过多讲解。
3、 Socket类的实现
Socket的实现的代码我们把它分为几部分函数:参数部分、UDP的打开、数据发送、数据接收以及关闭。这里使用的是套接字中的UdpClient 这个类来实现功能,实测OK。
(1) 参数部分
/// <summary>
/// TCP枚举类型
/// </summary>
public enum TCPType
{
TCPServer,
TCPClient,
UDP
}
//======> 自定义委托事件 捕获数据变化
public event EventHandler BufferReceChange; //Socket接收数据事件
public event EventHandler ConnStateChange; //Socket连接状态变化事件
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"; //发送数据目标IP
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(); //获取本机电脑一个IP
public string _localIP_Single
{
get { return localIP_Single; }
}
private string localIP_All = Utils.GetIPAddressAll(); //获取本机电脑所有IP
public string _localIP_ALL
{
get { return localIP_All; }
}
private string currentIP; //当前接收数据源 IP
public string _currentIP
{
get { return currentIP; }
set { currentIP = value; }
}
private int currentPort; //当前接收数据源端口 Port
public int _currentPort
{
get { return currentPort; }
set { currentPort = value; }
}
private string localIP;//本地IP
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; }
}
//网络通信类型 默认TCPServer
private TCPType tcpType = TCPType.UDP;
public TCPType _tcpType
{
get { return tcpType; }
set { tcpType = value; }
}
Thread UdpReceiveDataThread = null; //udp clienct接受数据线程
private UdpClient udp = new UdpClient();//实例化udp服务
private IPEndPoint localIpep = null;//本地IP结构实例
参数部分首先定义了委托的事件,程序指定了数据不为空就跳转到委托函数中运行。为UDP接收数据传递即时消息给到上层使用,就类似于中断消息处理。
连接状态、TCP类型等等各类参数,通过GET{}和SET{}方法来方便供外部赋值和获取。
最后,实例化线程和UDP服务。
(2) UDP的数据打开、接收、发送和关闭
//UPD通信 ======> 打开udp目标主机 、接受数据线程、 关闭UDP
/// <summary>
/// 打开UDP通信
/// </summary>
/// <returns>true or false</returns>
public bool UdpOpen()
{
try
{
UdpCLose();//关闭udp
localIpep = new IPEndPoint(IPAddress.Parse(localIP), localPort); // 本机IP和监听端口号
udp = new UdpClient(localIpep);//绑定本地UDP端口,localPort=0为自动分配
//创建UDP接收线程
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());
}
}
/// <summary>
/// UDP发送数据
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
public bool UdpSend(byte[] buffer)
{
try
{
//向目标IP发数据
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;
}
}
//UDP接收数据线程
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());
}
}
/// <summary>
/// 关闭udp
/// </summary>
private void UdpCLose()
{
try
{
udp.Close();
}
catch (Exception ex)
{
ExceptionContent = ex.Message + "\n" + ex.ToString();
//异常不做处理
return;
}
}
打开UdpOpen方法是这里最为关键的一环,首先要知道UDP也是有server(服务器)端和client(客户)端,而上位机这端一般作为的是client端(服务端也是能实现的),客户端是首先发起数据的一方。Open之后,客户端将持续监听自己的IP和端口。即这里IP地址是指定自己监听服务器将接收数据地址和端口,一般在服务器没有指定发送端口的时候,指定端口置0,也就是随机分配是没有问题的。 因为务器端的一般操作都会绑定(bind)客户端发送过来的数据中获取其中的IP和端口。但如果服务器指定了端口,那我们open的端口就必须是对方服务器指定的IP和端口的。这个要看实际的工程情况而定。因此,如代码所描述在线程开始前,localIpep 指定了自己的IP以及端口。
需要注意几点:
1、网线连接状态或者UDP服务是否被占用的问题异常处理;
2、发送接收单独建立socket类时候,要避免端口冲突;
3、及时要更新自己的数据连接状态。如:接收回应或心跳数据等。
发送数据UdpSend()就简单,将udp.Send(buffer, buffer.Length, _targetIP, _targetPort)函数的几个参数代入就能实现将buffer中的数据发送目标地址去。
接收的线程方法UdpRxThread最关键的就是udp.Receive(ref localIpep),这个函数将监听接收到的数据,否者将会一直等待,有数据时,将其拷贝出来即可,可以通过委托消息传递给上层显示。
退出UdpCLose()功能就是关闭Open状态,注意要及时关闭线程,避免反复开线程。
三、TCP Client上位机实现
1、 准备
如果我们要使用可靠的不允许有任何错误的通信时,我们就需要用到TCP连接通讯了。通过多次握手建立可靠的连接,HTTP、 FTP、 POP、 SSH就是基于这个来实现的,我们可以用C# 来实现TCP通信来做客户端上位机。
作为客户端的可靠连接,服务器端是首先要开启服务等待客户端连接的,对于客户端这条socket链路是唯一对应的。对于服务器端则可以建立多个客户端socket连接。
2、 TCP的连接、发送、接收和关闭实现
&emspForm主流程参数和UDP没有区别,调用实现原理也一样,我就不再阐述。 接下来就是TCP client端的连接、发送、接收和关闭的实现代码,大家可以参考。为了说清楚,我认为我注释得还比较详尽。
Thread TCPReceiveDataThread = null; //tcp clienct接收数据线程
public Socket _targetSocket = null; //连接目标套接字
public byte[] tcpRxBuf; //的使用接收到的数据 数据长度不确定
private const int MAX_RX_LEN = 512;
private byte[] _tdpReceiveData = new byte[MAX_RX_LEN]; //TCP clienct缓存接收到的直接数据,必须固定大小
/// <summary>
/// 连接服务器
/// </summary>
/// <returns>TRUE or FALSE</returns>
private bool ConnServer()
{
try
{
//InterNetwork: 地址簇IPV4
//Stream:套接字类型支持可靠、 双向、 基于连接的字节流
//Tcp: 指定通讯协议类型为传输控制协议
_targetSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//建立套接字
//IP地址转换格式
IPAddress address = IPAddress.Parse(_targetIP);
//IP地址端口绑定网络节点
IPEndPoint point = new IPEndPoint(address, _targetPort);
//建立与远程主机的连接,失败则进入异常
_targetSocket.Connect(point);
//连接后获取本地IP和端口
localIpAndPort = _targetSocket.LocalEndPoint.ToString();
//切割出IP
localIP = localIpAndPort.Split(':')[0];
//正常连接,建线程
TCPReceiveDataThread = new Thread(new ThreadStart(TcpClientReceiveDataThread));
//后台运行
TCPReceiveDataThread.IsBackground = true;
//线程名
TCPReceiveDataThread.Name = "Tcp客户端接收数据";
//线程开始
TCPReceiveDataThread.Start();
//置状态
connState = true;
return true;
}
catch (Exception ex)
{
//异常返回
_connState = false;
MessageBox.Show(ex.Message, "异常");
return false;
}
finally
{
//委托显示
this.OnConnStateChange(new EventArgs());
}
}
/// <summary>
/// tcp client接收数据线程
/// </summary>
private void TcpClientReceiveDataThread()
{
while (true)
{
try
{
//接收数据,并获取长度
int length = _targetSocket.Receive(_tdpReceiveData);
if (length == 0)
{
_connState = false;
return;
}
//实际使用BUF
tcpRxBuf = new byte[length];
Array.Copy(_tdpReceiveData, tcpRxBuf, length);
//委托显示
this.OnBufferReceChange(new EventArgs());
}
catch (Exception ex)
{
_connState = false;
}
}
}
/// <summary>
/// tcp client 发送数据
/// </summary>
/// <param name="buffer"> 发送的数据</param>
/// <returns>TRUE or FALSE</returns>
private bool tcpClientSendData(byte[] buffer)
{
try
{
_targetSocket.Send(buffer);//数据发送
return true;
}
catch (Exception ex)
{
_connState = false;
this.OnConnStateChange(new EventArgs());//委托显示
return false;
}
}
/// <summary>
/// 关闭TCP CLient
/// </summary>
private void closeTcpClent()
{
try
{
//关闭线程
ReceiveData.Abort();
//关闭socket类实例
_targetSocket.Close();
}
catch (Exception ex)
{
string err = ex.ToString();
}
finally
{
connState = false;
}
}
TCP的client客户端相对服务端要简单,将地址簇类型、套接字类型、传输协议类型以及对方服务器的IP和端口绑定就可以连接服务器了。一定要注意这里绑定的是服务器端的地址端口。而接收和发送线程甚至比UDP还要简单。需要注意的是TCP接收方法缓存空间_tdpReceiveData 必须大于接收数据最大长度,上面描述的代码定义的是512字节。通过数据返回的长度length来控制接收copy到实际使用的tcpRxBuf 中。读取后才会清空buf。 接收的数据可以通过事件委托给上层调用,也可以通过独立线程来解析或显示处理。
关于连接状态,可以自己建立连接状态的值,当然也可以简单地直接使用socket中的状态connState。
四、小结
最后,小结一下。
及结果反复学习和验证,完成了我想要的效果,问过有兴趣可以自己做一个DOME程序,自己研究,也可以用其它的网络通讯工具互相收发数据,条件受限同一台电脑上也可以实现。
无论是那种网络通讯方式,没有最好,只有最适合。希望我今天的收获可以成为今后的学习的坚实基础。
这里show一下我的DEMO程序界面吧,就此结束,如需要补充的,随时更新。