C#网络编程(TCP/UDP)
网络编程是现代软件开发中的核心技能之一它让设备与设备之间能够通过网络进行数据传输。无论是浏览网页、在线游戏还是即时通讯背后都离不开网络编程的支持。本文将带你从基础概念入手重点介绍两种最常用的传输层协议——UDP和TCP并通过 C# 代码示例展示它们的实际应用。1. 网络编程基础三要素在深入协议之前先了解网络编程的三个核心概念协议约束数据如何发送的规则好比两个人交流时使用的共同语言。IP地址标识网络中的目标设备例如192.168.0.100或127.0.0.1回环地址。端口标识目标设备上的具体应用例如 HTTP 服务默认使用 80 端口。2. UDP 协议只管发不管收2.1 UDP 的特点1. 无连接通信前不需要建立连接就像发短信直接发送。2. 不可靠不保证数据包一定到达也不保证顺序可能丢包。3. 高效没有连接建立和维护开销协议头小传输延迟低。4. 面向数据报数据以独立的数据包数据报形式发送和接收每个包有边界。2.2 典型应用场景视频流、音频通话、在线游戏、DNS 查询、广播/多播等对实时性要求高、允许少量丢包的场景比如我们在玩游戏视频通话的时候出现卡顿就是网络udp丢包的原因2.3 C# 实现 UDP 通信UdpClient 类这里先实现接收端代码来实现先收后发的功能// 绑定本地端点IP 端口 IPAddress ipAddress IPAddress.Loopback; // 自身回环地址127.0.0.1 IPEndPoint localEndPoint new IPEndPoint(ipAddress, 8080); UdpClient udpClient new UdpClient(localEndPoint); // 准备接收数据remoteEndPoint 会被填充为发送方的端点 IPEndPoint remoteEndPoint new IPEndPoint(IPAddress.Any, 0); byte[] receivedBytes udpClient.Receive(ref remoteEndPoint); Console.WriteLine($收到{Encoding.Default.GetString(receivedBytes)}); // 回复消息 string reply 收到我是客户端; byte[] sendBytes Encoding.Default.GetBytes(reply); udpClient.Send(sendBytes, sendBytes.Length, remoteEndPoint); udpClient.Close();1. 这里先绑定本地ip与端口地址这里设置为本机回环地址127.0.0.1端口设置为80802. 之后利用IPEndPoint类实例化本机地址与端口3.之后实例化一个udpclient对象用于监听发送到第二步实例化的端口上的udp报文4. 由于udp是无连接进行数据传输的我们这里定义完接收端口和ip之后我们就要找到发送端的地址了这里利用的是udp报文在发送的时候会将发送方的ip相关数据封装到数据报里面这时候我们接收方就可以通过解包这些数据来获取发送方的ip与端口号。5.所以这里我们实例化终端节点用来存放发送方的ip与端口6. 实例化之后使用udpClient的Receive阻塞队列的方法来监听是否有数据发送过来了如果发送过来解包方式一气呵成获取数据、ip、端口号如果没有听到数据就一直阻塞等待直到数据发送过来注意由于数据在传输的过程中是以字节的方式传输的在接收到数据之后我们要通过Encoding的方式将其转变成字符串进行输出这里可以选择UTF-8或者Default方式得到字符串等价的在发送的时候要将字符串转变为字符方式传输我们也可以更加完善这个方法来实现收发分离实现你发一句我回一句的方式利用多线程实现同时收发打造一个简单的控制台聊天程序。客户端先收后发IPAddress ip IPAddress.Loopback; IPEndPoint localEndPoint new IPEndPoint(ip, 8080); UdpClient udpClient new UdpClient(localEndPoint); IPEndPoint remoteEndPoint new IPEndPoint(IPAddress.Loopback, 9999); // 接收线程 new Thread(() { while (true) { byte[] content udpClient.Receive(ref remoteEndPoint); Console.WriteLine($服务器说{Encoding.Default.GetString(content)}); } }).Start(); // 发送线程 new Thread(() { while (true) { Console.Write(请输入内容); string str Console.ReadLine(); byte[] sendByte Encoding.Default.GetBytes(str); udpClient.Send(sendByte, sendByte.Length, remoteEndPoint); } }).Start(); Console.ReadKey();服务器端先发后收IPEndPoint localEndPoint new IPEndPoint(IPAddress.Loopback, 9999); UdpClient udpClient new UdpClient(localEndPoint); IPEndPoint remoteEndPoint new IPEndPoint(IPAddress.Parse(127.0.0.1), 8080); new Thread(() { while (true) { Console.Write(请输入内容); string str Console.ReadLine(); byte[] bytes Encoding.Default.GetBytes(str); udpClient.Send(bytes, bytes.Length, remoteEndPoint); } }).Start(); new Thread(() { while (true) { byte[] b udpClient.Receive(ref remoteEndPoint); Console.WriteLine($客户端说{Encoding.Default.GetString(b)}); } }).Start(); Console.ReadKey();这里我们使用线程的方式实现了监听接收发送数据的功能客户端与服务器都有两个线程同时进行监听与发送的操作我们这里简单测试没有使用IPAddress.Any的方式获取发送方ip与端口这里方便测试直接绑定目标与发送端口3. TCP 协议可靠的数据传输管道3.1 TCP 的特点面向连接通信前必须通过“三次握手”建立连接结束后“四次挥手”断开。可靠性通过确认应答、超时重传、序列号等机制保证数据不丢失、不重复、不乱序。基于字节流数据被视为无结构的字节流没有消息边界需要应用层处理粘包/分包。流量控制通过滑动窗口控制发送速率防止接收方缓冲区溢出。3.2 面试常问三次握手与四次挥手为什么是三次握手三次握手能确保双方都具有收发能力并且同步初始序列号。两次握手可能导致历史重复连接请求的误建立四次握手可以但没必要三次已足够完成双向确认。为什么是四次挥手因为 TCP 是全双工的每个方向都需要单独关闭。主动关闭方发送 FIN 表示不再发送数据被动方可能还有数据要发所以先回复 ACK待数据发送完再发 FIN。服务器端步骤创建TcpListener并绑定 IP 和端口。调用Start()开始监听。调用AcceptTcpClient()阻塞等待客户端连接返回与该客户端通信的TcpClient。通过TcpClient.GetStream()获取NetworkStream进行读写。操作完成后关闭资源。TcpListener listener new TcpListener(IPAddress.Loopback, 8080); listener.Start(); Console.WriteLine(服务器启动等待连接...); TcpClient client listener.AcceptTcpClient(); IPEndPoint remoteEndPoint client.Client.RemoteEndPoint as IPEndPoint; Console.WriteLine($客户端已连接{remoteEndPoint.Address}:{remoteEndPoint.Port}); NetworkStream stream client.GetStream(); // 先读取客户端消息 byte[] buffer new byte[1024]; int bytesRead stream.Read(buffer, 0, buffer.Length); Console.WriteLine($客户端说{Encoding.Default.GetString(buffer, 0, bytesRead)}); // 回复消息 string response 你好客户端; byte[] responseData Encoding.Default.GetBytes(response); stream.Write(responseData, 0, responseData.Length); stream.Close(); client.Close(); listener.Stop();大体步骤是和udp的发送接收相似的先创建接收端listener接受目的地址为本机的字节流调用Start方法开始监听之后实例化tcp客户端与客户端建立连接注意这里与UDP还有一点不同UDP是基于数据报进行数据传输的TCP是基于字节流进行传输的所以这里要通过NetWorkStream或者直接使用他的NetWorkStream的父类来接受这个字节流udp直接receive接着拼接字符串就好了通过实例化stream来接收数据之后就是同样的拼接字符串即可客户端步骤创建TcpClient对象。调用Connect()连接服务器。获取NetworkStream进行读写。关闭资源。TcpClient client new TcpClient(); client.Connect(127.0.0.1, 8080); NetworkStream stream client.GetStream(); // 先发送消息 string message 你好服务器; byte[] data Encoding.Default.GetBytes(message); stream.Write(data, 0, data.Length); // 再读取回复 byte[] buffer new byte[1024]; int bytesRead stream.Read(buffer, 0, buffer.Length); Console.WriteLine($服务器回复{Encoding.Default.GetString(buffer, 0, bytesRead)}); stream.Close(); client.Close();下面是我自己通过这个方式实现的一个收发检验的功能客户端发送账号与密码服务器你进行校验1.服务器的建立namespace TCP_Server { internal class Program { static void Main(string[] args) { TcpListener listener new TcpListener(IPAddress.Loopback, 8080); listener.Start(); Console.WriteLine(服务端启动等待连接...); TcpClient tcpClient listener.AcceptTcpClient(); IPEndPoint iPEndPoint tcpClient.Client.RemoteEndPoint as IPEndPoint; Console.WriteLine($有客户端进行连接IP{iPEndPoint.Address} 端口{iPEndPoint.Port}); Stream stream tcpClient.GetStream(); byte[] buffer new byte[1024]; int bytesRead stream.Read(buffer, 0, buffer.Length); string Msg Encoding.UTF8.GetString(buffer, 0, bytesRead).Trim(); string[] parts Msg.Split(|); string account parts[0]; string password parts[1]; Console.WriteLine($接收到客户端发来的账号:{account}); Console.WriteLine($接收到客户端发来的密码:{password}); Valid(account, password, stream); stream.Close(); tcpClient.Close(); Console.ReadKey(); } public static void Valid(string acc, string pas, Stream stream) { string response; byte[] buffer; if (acc hongmao pas 123456) { response 登录成功; buffer Encoding.UTF8.GetBytes(response); stream.Write(buffer, 0, buffer.Length); } else { response 登录失败; buffer Encoding.UTF8.GetBytes(response); stream.Write(buffer, 0, buffer.Length); } } } }2.客户端的建立namespace TCP_Client { internal class Program { static void Main(string[] args) { TcpClient tcpClient new TcpClient(); tcpClient.Connect(127.0.0.1, 8080); Console.WriteLine(客户端正在启动中...); Console.WriteLine(客户端已启动); //调用TcpClient的GetStream得到字节流 Stream stream tcpClient.GetStream(); //准备账号和密码 Console.Write(请输入你的账号); string account Console.ReadLine(); Console.Write(请输入你的密码); string password Console.ReadLine(); //将账号与密码从字符串转变为字节 string message account | password \n; byte[] data Encoding.UTF8.GetBytes(message); //将账号与密码传递给服务器 stream.Write(data, 0, data.Length); Console.WriteLine(账号密码发送成功); //准备获取服务器发来的消息 byte[] receiveBuffer new byte[4096]; int byteRead stream.Read(receiveBuffer, 0, receiveBuffer.Length); string serverResponse Encoding.UTF8.GetString(receiveBuffer); Console.WriteLine($服务器发来消息{serverResponse}); stream.Close(); tcpClient.Close(); Console.ReadKey(); } } }感悟我在实现发送账号密码这个功能的时候想着在客户端做两个发送服务器做两个接收来分别获取账号与密码但是这是行不通的原因如下TCP 的粘包问题TCP 是面向字节流的协议它不保留应用程序的消息边界。也就是说多次调用Send发送的数据可能会被 TCP 协议组合成一个数据包发送出去粘包也可能因为网络 MTU 限制被拆分成多个包拆包。粘包现象客户端先发送账号比如 Alice再发送密码比如 123456TCP 可能将这两个字符串合并成一个数据流Alice123456发给服务器。服务器第一次Read可能一次性读到了Alice123456导致第二次Read无数据可读阻塞或返回 0。拆包现象反之如果一次发送的数据太大可能分成多个包导致第一次Read只读到部分数据需要多次读取才能完整接收一个消息。这里再解释一下udp方面的UDP 的丢包与乱序UDP 是无连接、不可靠的协议每个数据报独立传输。丢包账号包或密码包可能在网络中丢失导致服务器只收到其中一个程序无法继续。乱序网络情况复杂时后发的密码包可能先到达服务器导致服务器先收到密码再收到账号完全错乱。没有消息边界问题UDP 本身保留消息边界每个Receive正好对应一个完整的发送包但丢包和乱序仍会导致逻辑错误。1. 所以这里我在实现这个方法的时候在客户端获取到用户账号还有密码的时候通过一个分隔符将他们整合成了一段数据这样到接收端只要将其拆分就好了2. 但是问题到这里还没有解决我在测试数据的时候发现即使发送了正确的账号还有密码都没法实现验证通过检查发现在代码里面我们定义了一个缓冲数组来存储stream读取到的字符byte[] buffer new byte[1024]; int bytesRead stream.Read(buffer, 0, buffer.Length);缓冲区我们设置的大小为1024B在之后的stream.read的时候会把1024B全部读入没有数据的地方会被潜在存储为\0\0\0的没有数据的状态string Msg Encoding.UTF8.GetString(buffer, 0, bytesRead); string[] parts Msg.Split(|);所以我们在通过分隔符分开数据的时候数据被分成了hongmao123456\0\0\0...的形式导致登录不成功于是这时候我们要通过Tirm的方式删去没有数据的部分string Msg Encoding.UTF8.GetString(buffer, 0, bytesRead).Trim(); string[] parts Msg.Split(|);这样我们就会获取到干净的账号还有密码啦4. UDP 与 TCP 对比总结特性UDPTCP连接无连接面向连接三次握手可靠性不可靠可能丢包、乱序可靠确认、重传、顺序保证数据边界保留消息边界数据报无边界字节流需应用层处理粘包效率高低延迟相对较低连接维护、确认开销流量控制无有滑动窗口机制典型应用音视频、实时游戏、DNS文件传输、网页浏览、邮件