网络编程基础
TCP/IP体系结构和特点
其中,各层的特点如下:
网络接口层:是TCP/IP的最低层,负责接收IP数据报并通过网络发送之,或者从网络上接收物理帧,抽出IP数据报,交给IP层。
网络层:负责相邻计算机之间的通信。其功能包括三方面:
(1) 处 理 来 自 传 输 层 的 分 组 发 送 请 求 , 收到请求后,将分组装入I P 数据报,填充报头,选择去往信宿机的路径,然后将数据报发往适当的网络接口。
(2) 处 理 输 入 数 据 报 : 首 先 检 査 其 合 法 性 , 然 后 进 行 路 由 — — 假 如 该 数 据 报 已到达信宿机,则去掉报头,将剩下部分交给适当的传输协议;假如该数据报尚未到达信宿,则转发该数据报。
(3) 处理路径、流控、拥塞等问题。
传输层 : 提供应用程序间的通信。其功能包括:格式化信息流和提供可靠传输。为实现后者,传输层协议规定接收端必须发回确认,并且假如分组丢失,必须重新发送。
应用层:向用户提供一组常用的应用程序,如电子邮件、文件传输访问、远程登录等。
网络编程的重要术语
与网络编程相关的重要术语主要有套接字、网间进程、端口和客户机服务器。
套接字 socket
套接字成了网络通信的基石 ,是支持T C P / I P 协议簇的网络通信的基本操作单元,成为网络之间的编程界面。理解套接字的好方法是把它看做网络上不同主机进程之间相互通信的端点。从程序员角度来看,它是应用程序和网络设备的一个接口,是特殊的I/O。
套接字的类型
(1)流式套接字(SOCK_STREM)
提供面向连接、可靠的数据传输服务。保证了数据能够实现无差错、无重复地发送,且按顺序发送。
使用TCP协议。
适用于可靠性高、不追求实时性的场合,如:文件传输。但它不支持广播和多播。
(2)数据报套接字(SOCK_DGRAM)
类似于邮件系统,提供一个无连接服务。通信双方不需要建立连接,数据就可以发送到指定的套接字,并可以从指定的套接字接收数据。数据包以独立的方式被发送,不保证数据。
使用UDP协议。
适用于实时性要求高、不追求可靠性的场合,如:IP电话、网络视频会议等。支持广播和多播。
(3)原始套接字(SOCK_RAW)
原始套接字可以读写内核没有处理的IP数据报。使用它主要是为了避开TCP/IP的处理机制,允许对低层协议,如IP、ICMP直接访问,被传送的数据报可以被直接传送给需要它的应用程序。
网间进程、端口
TCP/IP提出端口。端口是一种抽象的软件结构,类似于文件描述符,每个端口都有一个端口号用来区别。
端口号占16位,0~65535。TCP和UDP的端口号相互独立(即使相同也不冲突)
- 端口号全局分配:又称静态分配,由机构根据需要统一分配。
- 端口号动态分配:客户根据需要动态申请和使用,除了静态端口号,都可以作为动态分配。
网络进程采用三级寻址,即特定网络(TCP/UDP)、主机地址(IP地址)、进程标识(端口号)。
- 半相关:(协议、本地地址、本地端口号)—— 本地通信进程
- 全相关:(协议、本地地址、本地端口号、远地地址、远地端口号)—— 网间通信进程
客户机/服务器模式 C/S
C/S模式:客户机向服务器发送服务请求,服务器接收到请求后,提供相应的服务。
服务器必须要有并发请求的能力。因为 多客户机——>一台服务器
并发服务器的工作原理
C#网络编程基础知识
常用网络组件
寻找IP地址的类和方法
与 I P 地址相关的类有IPAddress类、IPHostEntry类、IPEndPoint类和Dns类。
IPAddress类的主要属性和方法如下:
寻找IP地址的方法:
一般可以利用Dns类的GetHostName方法找到本地系统主机名,再用该类的GetHostByName方法找到主机的IP地址
数据流的类型和应用
在VS. NET平台上,包括了以下3种数据流类型:
•网络流NetworkStream,命名空间是System.Net.Sockets,用于网络数据的读/写操作;
•内存流MemoryStream,命名空间是System.IO,用于内存数据的处理和转换;
•文件流FileStream,命名空间是System.IO,用于文件的读/写操作。
多线程技术
在Windows中,系统能够同时运行多个程序,每一个正在运行的程序称为一个进程。 同一个进程又可以分成若干个独立的执行流,称为线程。线程是操作系统向其分配处理器时间的基本单位。线程可执行进程的任何一部分代码,包括当前由另一线程执行的部分。
Thread、ThreadPool、Task
套接字编程原理
根据套接字的不同类型,可以将套接字调用分为面向连接服务和无连接服务。
面向连接服务
•数据传输过程必须经过建立连接、维护连接和释放连接3个阶段;
•在传输过程中,各分组不需要携带目的主机的地址;
•可靠性好,但由于协议复杂,通信效率不高。
面向无连接服务
•不需要连接的各个阶段;
•每个分组都携带完整的目的主机地址,在系统中独立传送;
•由于没有顺序控制,所以接收方的分组可能出现乱序、重复和丢失现象:
•通信效率高,但不能确保可靠性。
Socket类的基本使用
简单示例:数据报套接字【面向无连接】
static void Main(string[] args) {
byte[] data = new byte[1024];
int dataLength = 1024;
// 创建实例对象
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 设置本地网络端点(IP地址+接收端口号)
IPEndPoint myHost=new IPEndPoint(IPAddress.Any, 8000);
socket.Bind(myHost);//将本地端点与套接字绑定
// 定义远程网络端点
IPEndPoint remoteIPEnd = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8000);
EndPoint remoteHost = (EndPoint)remoteIPEnd;//IPEndPoint->EndPoint类型
Console.WriteLine("输入要发送的信息:");
string message=Console.ReadLine();
// 字符串=>字节数组
data=System.Text.Encoding.UTF8.GetBytes(message);
// 向远程终端发送信息
socket.SendTo(data, remoteHost);
while (true) {
Console.WriteLine("等待接收...");
//本地网络端点接收来自远程终端的数据,返回接收的字节数
dataLength = socket.ReceiveFrom(data, ref remoteHost);
//字节数组=>字符串
string getMessage=System.Text.Encoding.UTF8.GetString(data);
Console.WriteLine("接收到信息:"+getMessage);
//如果收到"exit",则跳出循环
if (getMessage == "exit") {
break;
}
Console.WriteLine("输入回送信息(exit退出):");
getMessage=Console.ReadLine();
data = System.Text.Encoding.UTF8.GetBytes(getMessage);
socket.SendTo(data, remoteHost);
}
//关闭套接字
socket.Close();
Console.WriteLine("对方已退出,请按enter键结束");
Console.ReadLine();
}
基于UDP协议的程序设计
什么是UDP协议?
UDP是一种简单、面向数据报的无连接协议,提供的是不一定可靠的传输服务。
单播(一对一)
与TCP通信不同,UDP通信是不分服务端和客户端的,通信双方是对等的。
为了描述方便,我们把通信双方称为发送方和接收方
// 发送方
string sendString = null;//要发送的字符串
byte[] sendData = null;//要发送的字节数组
UdpClient client = null;
IPAddress remoteIP = IPAddress.Parse("127.0.0.1");
int remotePort = 11000;
IPEndPoint remotePoint = new IPEndPoint(remoteIP, remotePort);//实例化一个远程端点
while (true) {
sendString = Console.ReadLine();
sendData = Encoding.Default.GetBytes(sendString);
client = new UdpClient();
client.Send(sendData, sendData.Length, remotePoint);//将数据发送到远程端点
client.Close();//关闭连接
}
// 接收方
UdpClient client = null;
string receiveString = null;
byte[] receiveData = null;
//实例化一个远程端点,IP和端口可以随意指定,等调用client.Receive(ref remotePoint)时会将该端点改成真正发送端端点
IPEndPoint remotePoint = new IPEndPoint(IPAddress.Any, 0);
while (true) {
client = new UdpClient(11000);
receiveData = client.Receive(ref remotePoint);//接收数据
receiveString = Encoding.Default.GetString(receiveData);
Console.WriteLine(receiveString);
client.Close();//关闭连接
}
广播(一对所有)
本地广播:同时向本地子网中的多台计算机发送消息,其他网络不会受到本地广播的影响。
例如,对于10.1.1.0 (255.255.255.0 )网段,其广播地址为10.1.1.255 (255 即为 2 进制的 11111111 ),当发出一个目的地址为10.1.1.255 的数据包时,它将被分发给该网段上的所有计算机。广播地址应用于网络内的所有主机。
用途:网络会议,网络信息通告,广告
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Test
{
class Program
{
static void Main(string[] args)
{
UdpClient UDPsend = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
IPEndPoint endpoint = new IPEndPoint(IPAddress.Broadcast, 8080);
//其实 IPAddress.Broadcast 就是 255.255.255.255
//下面代码与上面有相同的作用
//IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse("255.255.255.255"), 8080);
byte[] buf = Encoding.Default.GetBytes("This is UDP broadcast");
Thread receThread = new Thread(new ThreadStart(RecvThread));
receThread.IsBackground = true;
receThread.Start();
while (true)
{
UDPsend.Send(buf, buf.Length, endpoint);
Thread.Sleep(1000);
}
}
static void RecvThread()
{
UdpClient UDPrece= new UdpClient(new IPEndPoint(IPAddress.Any, 8080));
IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0);
while (true)
{
byte[] buf = UDPrece.Receive(ref endpoint);
string msg = Encoding.Default.GetString(buf);
Console.WriteLine(msg);
}
}
}
}
组播(多播、一对一组)
多播的基本思想是源主机只发送一份数据,这份数据中的目的地址为多播组地址多播组中的所有接收者都可接收到同样的数据拷贝,并且只有多播组内的主机目标主机可以接收该数据网络中其它主机不能收到。
用途:开黑、视频聊天、多人协作文件
组播IP地址 IP组播地址用于标识一个IP多播组。D类地址用于多播。即224.0.0.0至239.255.255.255之间的IP地址,并被划分为局部连接多播地址、预留多播地址和管理权限多播地址
UdpClient 类的组播方法
(1)JoinMulticastGroup (2)DropMulticastGroup
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Test
{
class Program
{
static void Main(string[] args)
{
UdpClient client = new UdpClient(5566);
client.JoinMulticastGroup(IPAddress.Parse("234.5.6.7"));
IPEndPoint multicast = new IPEndPoint(IPAddress.Parse("234.5.6.7"), 7788);
byte[] buf = Encoding.Default.GetBytes("Hello from multicast");
Thread t = new Thread(new ThreadStart(RecvThread));
t.IsBackground = true;
t.Start();
while (true)
{
client.Send(buf, buf.Length, multicast);
Thread.Sleep(1000);
}
}
static void RecvThread()
{
UdpClient client = new UdpClient(7788);
client.JoinMulticastGroup(IPAddress.Parse("234.5.6.7"));
IPEndPoint multicast = new IPEndPoint(IPAddress.Parse("234.5.6.7"), 5566);
while (true)
{
byte[] buf = client.Receive(ref multicast);
string msg = Encoding.Default.GetString(buf);
Console.WriteLine(msg);
}
}
}
}
基于TCP协议的程序设计
什么是TCP协议?
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
(1)面向连接:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
(2)可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端,不会出现丢失或乱序;
(3)字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。
TCP协议通信特点
(1)三次握手
(2)四次挥手
阻塞、非阻塞和同步、异步的区别
(1)阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待;(程序在等待调用结果(消息,返回值)时的状态)。
(2)同步,异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写;(消息通信机制)。
阻塞/非阻塞模式应用
阻塞模式
什么是阻塞模式?【Socket 默认就是阻塞模式】
执行套接字的调用函数只有在得到结果之后才会返回。在那之前,当前线程会被挂起。
特点:
- 结构简单,容易实现,编程逻辑清晰
- 在读操作时可能永远阻塞,进程效率较低
提升效率:
(1)超时控制方法
(2)套接字多路复用方法 Select()
(3)调用异步选择函数 AsyncSelect,进入异步非阻塞模式。
阻塞函数:
(1)read()、recv()、recvfrom()、recvmsg()
(2)write()、send()、sendto()、sendmsg()
(3)accept()
(4)connect()
非阻塞模式
什么是非阻塞模式?
执行套接字的调用函数,即使不能得到结果,也会立即返回。不会阻塞当前线程。
特点:
- 保证进程永不阻塞,同时进程可同时处理多个描述符的输入/输出操作。
- 编程复杂,占用CPU时间较长
如何避免使用阻塞模式?
(1)非阻塞套接字 —— sock.Blocking=false
(2)异步套接字 —— 采用异步回调AsyncCallback 委托
同步套接字编程技术
什么是同步?
客户机发送请求后,必须获得服务器的回应,才能发送下一个请求。
实现过程
同步套接字编程技术主要用于客户端、服务器之间建立连接和相互传输数据。
(1)服务器打开监听,等待客户发送连接请求。
(2)客户机发送请求连接
(3)服务器收到请求后,产生一个新的套接字
(4)客户机使用套接字与刚产生的新的套接字进行通信——发送数据、接收数据
示例代码
服务器方
namespace Server {
public partial class Form1 : Form {
private Socket socket; // 套接字
private Socket newSocket; // 套接字
Thread thread; // 线程
public Form1() {
InitializeComponent();
}
// 开始监听
private void btnXStartListen_Click(object sender, EventArgs e) {
this.btnXStartListen.Enabled = false;
IPAddress ip = IPAddress.Parse(this.textBoxXIP.Text);
IPEndPoint server = new IPEndPoint(ip, Int32.Parse(this.textBoxXPort.Text));
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(server);
// 监听客户端连接
socket.Listen(10);
newSocket = socket.Accept();
// 显示客户IP和端口号
this.listBoxAdvState.Items.Add("与客户" + newSocket.RemoteEndPoint.ToString() + "建立连接");
// 建立一个线程接收客户信息
thread = new Thread(new ThreadStart(AcceptMessage));
thread.Start();
}
// 线程执行的方法
private void AcceptMessage() {
while (true) {
try {
NetworkStream netStream = new NetworkStream(newSocket);
byte[] datasize = new byte[4];
netStream.Read(datasize, 0, 4);
int size = System.BitConverter.ToInt32(datasize, 0);
Byte[] message = new byte[size];
int dataleft = size;
int start = 0;
while (dataleft > 0) {
int recv = netStream.Read(message, start, dataleft);
start += recv;
dataleft -= recv;
}
this.richTextBoxExAccept.Rtf = System.Text.Encoding.Unicode.GetString(message);
} catch {
this.listBoxAdvState.Items.Add("与客户端断开连接");
break;
}
}
}
// 数据发送
private void btnXSend_Click(object sender, EventArgs e) {
string str = this.richTextBoxExSend.Rtf;
int i = str.Length;
if (i == 0) {
return;
} else {
// 因为str为Unicode编码,每个字符占字节,所以实际字节数应 *2
i *= 2;
}
byte[] datasize = new byte[4];
// 将位整数值转换为字节数组
datasize = System.BitConverter.GetBytes(i);
byte[] sendbytes = System.Text.Encoding.Unicode.GetBytes(str);
try {
NetworkStream netStream = new NetworkStream(newSocket);
netStream.Write(datasize, 0, 4);
netStream.Write(sendbytes, 0, sendbytes.Length);
netStream.Flush();
this.richTextBoxExSend.Rtf = "";
} catch {
MessageBox.Show("无法发送!");
}
}
// 停止监听事件
private void btnXStopListen_Click(object sender, EventArgs e) {
this.btnXStartListen.Enabled = true;
try {
socket.Shutdown(SocketShutdown.Both);
socket.Close();
if (newSocket.Connected) {
newSocket.Close();
thread.Abort();
}
} catch {
MessageBox.Show("监听尚未开始,关闭无效!");
}
}
// 当服务器方没有停止监听状态,并且直接关闭应用程序时,可以先
private void Form1_FormClosing(object sender, FormClosingEventArgs e) {
try {
socket.Shutdown(SocketShutdown.Both);
socket.Close();
if (newSocket.Connected) {
newSocket.Close();
thread.Abort();
}
} catch {
}
}
}
}
客户端方
namespace Client {
public partial class Form1 : Form {
private Socket socket;
private Thread thread;
public Form1() {
InitializeComponent();
}
// 请求连接
private void buttonXRequest_Click(object sender, EventArgs e) {
IPAddress ip = IPAddress.Parse(this.textBoxXIP.Text);
IPEndPoint server = new IPEndPoint(ip, Int32.Parse(this.textBoxXPort.Text));
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try {
socket.Connect(server);
} catch {
MessageBox.Show("连接服务器失败!");
return;
}
this.buttonXRequest.Enabled = false;
this.listBoxAdvState.Items.Add("与服务器连接成功!");
Thread thread = new Thread(new ThreadStart(AcceptMessage));
thread.Start();
}
private void AcceptMessage() {
while (true) {
try {
NetworkStream netStream = new NetworkStream(socket);
byte[] datasize = new byte[4];
netStream.Read(datasize, 0, 4);
int size = System.BitConverter.ToInt32(datasize, 0);
Byte[] message = new byte[size];
int dataleft = size;
int start = 0;
while (dataleft > 0) {
int recv = netStream.Read(message, start, dataleft);
start += recv;
dataleft -= recv;
}
this.richTextBoxExReceive.Rtf = System.Text.Encoding.Unicode.GetString(message);
} catch {
this.listBoxAdvState.Items.Add("服务器断开连接!");
break;
}
}
}
private void buttonXSend_Click(object sender, EventArgs e) {
string str = this.richTextBoxExSend.Rtf;
int i = str.Length;
if (i == 0) {
return;
} else {
i *= 2;
}
byte[] datasize = new byte[4];
// 将位整数值转换为字节数组
datasize = System.BitConverter.GetBytes(i);
byte[] sendbytes = System.Text.Encoding.Unicode.GetBytes(str);
try {
NetworkStream netStream = new NetworkStream(socket);
netStream.Write(datasize, 0, 4);
netStream.Write(sendbytes, 0, sendbytes.Length);
netStream.Flush();
this.richTextBoxExSend.Text = "";
} catch {
MessageBox.Show("无法发送!");
}
}
private void buttonXClose_Click(object sender, EventArgs e) {
try {
socket.Shutdown(SocketShutdown.Both);
socket.Close();
this.listBoxAdvState.Items.Add("与主机断开连接");
thread.Abort();
} catch {
MessageBox.Show("尚未与主机连接,断开无效!");
}
this.buttonXRequest.Enabled = true;
}
}
}
异步套接字编程技术
什么是异步?
客户机发送请求后,不必等服务器回应就能发送下一个请求。
先使用Begin()方法,然后在AsyncCallback委托提供的方法中调用End()方法结束操作。
异步套接字主要使用的方法:
实现过程
(1)客户机使用BeginConnect方法发出连接请求,然后异步执行ConnectServer,
获得连接状态后,再调用EndConnect方法完成连接
(2)服务器接收连接请求。调用Listen()方法之前与同步套接字一样。
程序执行到接收连接请求时,使用异步套接字方法BeginAccept()。
(3)服务器发送送和接收数据。一旦服务器接收到一个客户机连接请求,AsyncCallback委托将自动调用AcceptConnection方法,获得返回信息。并调用EndAccept()方法接收请求。
(4)对应客户端,其异步发送与接收的使用方法与服务器几乎相同。
基于TcpClient和TcpListener的编程
TcpClient ⇒ 客户机 TcpListener ⇒ 服务器
TcpClient类的使用
属性、方法:
TcpListener类的使用
用于监听和接收传入的连接请求。
(1)创建实例(2)监听连接(3)接收连接请求(4)收发数据(5)停止服务
属性、方法: