在实现网络传输时,人们把通信问题划分成了多个小问题,然后为每个小问题设计了一个单独的协议,使得每个协议都比较简单。
国际化标准组织(ISO)和国际电报电话咨询委员会(CCITT)共同制定了开放系统互联的七层参考模型,即把网络传输划分为7个独立的层次。
ISO七层网络模型
应用层(应用层、表示层、会话层)
把用户的数据转化成二进制流传递给传输层。
传输层
以TCP协议为例:
TCP提供了IP环境下的数据可靠传输,它实现了数据流传送、可靠性校验、流量控制等功能。
传输层的数据转换:数据前添加一个TCP首部。
网络层
以IP协议为例:
IP协议用于将多个包的交换网络连接起来,它在源地址和目的地址之间传送数据包,它还提供了对数据进行重新组装的功能,以适应不同网络对包大小的不同要求。
网络层的数据转换:IP协议会给数据加上目的地地址(IP地址和端口)等信息,必要时还会拆分数据。
数据链路层
传输中若发生差错,为了达成只将有错的有限数据进行重发,数据链路层将二进制流组合成帧,然后以帧为单位进行传送。每个帧除了要传送的数据外,还包括校验码,以使接收方能发现传输中的差错。
帧首部|数据 帧首部|数据 帧首部|数据
物理层
物理层传输就是数据通过物理介质进行传输的过程,物理介质包括电缆、光纤等物质。这些数据通过物理介质传输到目的地,目的地再依照与上述相反的过程进行解析,最后得到用户的数据。
IP与端口
网络上的计算机都是通过IP地址进行识别的,应用程序通过通信端口彼此通信。
IP地址
端口
“端口“是英文port的意译,是设备与外界通信交流的出口,每台计算机可以分配0到65535共65536个端口。其中0到1023号端口称为众所周知的端口号,它们被分配给一些固定的服务,比如80端口分配给WWW服务,21端口分配给FTP服务。
C#中的相关类型
C#的System.Net命名空间提供了两个IP和端口相关的类IPAddress和IPEndPoint。
IPAddress : 指示IP地址,如“127.0.0.1”。IPEndPoint : 指示IP地址和端口对的组合,如“127.0.0.1:80”。
IPAddress的常用属性 | 说明 |
---|---|
IPAddress.Any | 使用机器上一个可用的IP来初始化这个IP地址对象 |
IPAddress.Parse | 根据IP地址创建IPAddress对象,如IPAddress.Parse("192.168.0.1") |
IPEndPoint的常用属性 | 说明 |
---|---|
Address | 获取或设置终结点的IP地址 |
Port | 获取或设置终结点的端口号 |
TCP协议
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,而与TCP相对应的UDP协议则是无连接的、不可靠的协议(但传输效率比TCP高)。
TCP连接的建立
TCP是面向连接的,无论哪一方在向另一方发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的是同步连接双方的序列号和确认号,并交换TCP窗口大小的信息。
TCP的数据传输
发送一个数据后,发送方并不能确保数据一定会被接收方接收。于是发送方会等待接收方的回应,如果太长时间没有收到回应,发送方会重新发送数据。
TCP连接的终止
客户端和服务器通过三次握手建立了TCP连接以后,待数据传送完毕,便要断开连接。与三次握手相似,TCP通过“四次挥手”来确保双端都断开了连接。
第一次挥手:主机1(可以是客户端也可以是服务器)向主机2发送一个终止信号(FIN),此时,主机1进入FIN_WAIT_1状态,它没有需要发送的数据,只需要等待主机2的回应。
第二次挥手:主机2收到了主机1发送的终止信号(FIN),向主机1回应一个ACK。收到ACK的主机1进入FIN_WAIT_2状态。
第三次挥手:在主机2把所有数据发送完毕后,主机2向主机1发送终止信号(FIN),请求关闭连接。
第四次挥手:主机1收到主机2发送的终止信号(FIN),向主机2回应ACK。然后主机1进入TIME_WAIT状态(等待一段时间,以便处理主机2的重发数据)。主机2收到主机1的回应后,关闭连接。至此,TCP的四次挥手便完成了,主机1和主机2都关闭了连接。
Socket套接字
Socket连接的流程
套接字是支持TCP/IP协议网络通信的基本操作单元,可以将套接字看作不同主机间的进程双向通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常会和同一个域中的套接字交换数据(数据交换也可能会穿越域的界限,但这时一定要执行某种解释程序)。各种进程使用这个相同的域用Internet协议来进行互相之间的通信。
1)开启一个连接之前,需要先完成Socket和Bind两个步骤。Socket是新建一个套接字,Bind指定套接字的IP和端口(客户端在调用Connect时会由系统分配端口,因此可以省去Bind)。
2)服务端通过Listen开启监听,等待客户端接入。
3)客户端通过Connect连接服务器,服务端通过Accept接收客户端连接。在connect-accept过程中,操作系统将会进行三次握手。
4)客户端和服务端通过Send和Receive发送和接收数据,操作系统将会完成TCP数据的确认、重发等步骤。
5)通过Close关闭连接,操作系统会进行四次挥手。
Socket类
System.Net.Sockets命名空间的Socket类为网络通信提供了一套丰富的方法和属性。
Socket类的一些常用属性 | 说明 |
---|---|
AddressFamily | 获取Socket的地址族 |
Available | 获取已经从网络接收且可供读取的数据量 |
Blocking | 获取或设置一个值,该值指示Socket是否处于阻止模式 |
Connected | 获取一个值,该值指示Socket是否连接 |
IsBound | 指示Socket是否绑定到特定的本地端口 |
OSSupportsIPv6 | 指示操作系统和网络适配器是否支持Internet协议第6版(IPv6) |
ProtocolType | 获取Socket的协议类型 |
SendBufferSize | 指定Socket发送缓冲区的大小 |
SendTimeout | 发送数据(Send)的超时时间 |
ReceiveBufferSize | 指定Socket接收缓冲区的大小 |
ReceiveTimeout | 接收数据(Receive)的超时时间 |
Ttl | 指定Socket发送的Internet协议(IP)数据包的生存时间(TTL)值 |
同步Socket程序
现在,编写一套简单的网络程序,这套网络程序分为客户端和服务端两个部分。客户端发送一行文本给服务端,服务端收到后将文本稍加改动后发回客户端。
编写服务端程序
服务器遵照Socket通信的基本流程,先创建Socket,再调用Bind绑定IP地址和端口号,之后调用 Listen等待客户端连接。最后在while循环中调用Accept接收客户端的连接,并回应消息。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace SocketServer
{
class MainClass
{
public static void Main(string[] args)
{
// 1. 创建服务器端Socket实例 (AddressFamily.InterNetwork : 使用IPv4 / AddressFamily.InterNetworkV6 : 使用IPv6)
Socket server_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 2. 绑定IP地址和端口号
IPAddress ipAddr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 1234);
//IPAddress[] ips = Dns.GetHostAddresses(Dns.GetHostName());
//Console.WriteLine("ipaddress length : " + ips.Length + ", ip :" + ips[0]);
//IPEndPoint ipEndPoint = new IPEndPoint(ips[0], 22222);
server_socket.Bind(ipEndPoint);
// 3. 开始监听
server_socket.Listen(0);
Console.WriteLine("服务器启动成功");
while (true)
{
// 4. 接收客户端的请求
Console.WriteLine("正在接收客户端的请求。。。");
Socket client = server_socket.Accept(); // 阻塞方法 会返回连接的客户端的套接字
Console.WriteLine("[服务器]Accept");
IPEndPoint clientIP = (IPEndPoint)client.RemoteEndPoint; // 获取客户端的端点信息
Console.WriteLine("connect with client:" + clientIP.Address + ", at port:" + clientIP.Port);
// 5. 给客户端发送信息
//string welcome = "welcome connect";
//byte[] data = Encoding.ASCII.GetBytes(welcome);
//client.Send(data, data.Length, SocketFlags.None);
// 6. 接收客户端的消息
byte[] data = new byte[1024];
while (true)
{
int recv = client.Receive(data);
string recvStr = Encoding.UTF8.GetString(data, 0, recv);
Console.WriteLine("接收到消息:" + recvStr);
if(recvStr.ToLower() == "exit")
{
break;
}
data = Encoding.UTF8.GetBytes(clientIP.ToString() + " : " + recvStr);
client.Send(data);
}
client.Close();
Console.WriteLine("客户端:" + clientIP + " 断来连接");
}
}
}
}
Socket server_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
AddressFamily : 指定Socket用来解析地址的寻址方案。例如:InterNetWork指示当Socket使用一个IPv4地址连接。
ProtocolType用于指明协议
常用的协议 | 含义 |
---|---|
Ggp | 网关到网关协议 |
Icmp | 网际消息控制协议 |
IcmpV6 | 用于IPv6的Internet控制消息协议 |
Idp | Internet数据报协议 |
Igmp | Internet组管理协议 |
IP | Internet协议 |
Internet | 数据包交换协议 |
PARC | 通用数据包协议 |
Raw | 原始IP数据包协议 |
Tcp | 传输控制协议 |
Udp | 用户数据报协议 |
UnKnown | 未知协议 |
Unspecified | 未指定的协议 |
客户端程序
异步Socket程序
服务端
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
namespace _1_服务端
{
class Conn
{
const int BUFFER_SIZE = 1024;
public Socket socket;
public bool isUse = false;
public byte[] buffer;
public int position;
public Conn()
{
buffer = new byte[BUFFER_SIZE];
position = 0;
isUse = false;
}
public void Init(Socket socket)
{
this.socket = socket;
}
public int BufferRemainSize
{
get
{
return BUFFER_SIZE - position;
}
}
public string GetAddress()
{
return socket.LocalEndPoint.ToString();
public void Close()
{
if(!isUse) return;
Console.WriteLine("断开连接:" + GetAddress());
socket.Close();
isUse = false;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
namespace _1_服务端
{
class Server
{
Socket serverSocket;
const int MAX_CLIENT_NUM = 50;
// 客户端连接
Conn[] conns;
public Server()
{
conns = new Conn[MAX_CLIENT_NUM];
for (int i = 0; i < conns.Length; i++)
{
conns[i] = new Conn();
}
}
// 获取可用的客户端
public int GetNewIndex()
{
if (conns == null) return -1;
for (int i = 0; i < conns.Length; i++)
{
if(conns[i] == null)
{
conns[i] = new Conn();
return i;
}
else if(conns[i].isUse == false)
{
return i;
}
}
return -1;
}
public void Start(string host, int port)
{
// 创建服务端套接字
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 绑定端点
IPAddress ipAddress = IPAddress.Parse(host);
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
serverSocket.Bind(ipEndPoint);
// 监听
serverSocket.Listen(MAX_CLIENT_NUM);
// 接收
Console.WriteLine("服务器开始接收");
// 开始一个异步操作来接受一个传入的连接尝试
serverSocket.BeginAccept(AsyncCb, null);
}
// 接收回调
private void AsyncCb(IAsyncResult ar)
{
try
{
Console.WriteLine("服务器接收异步调用");
// 异步接受传入的连接尝试,并创建新的socket来处理远程主机的通信
Socket clientSocket = serverSocket.EndAccept(ar);
int index = GetNewIndex();
// 连接已满
if (index < 0)
{
clientSocket.Close();
Console.WriteLine("连接已满");
}
else
{
Conn conn = conns[index];
conn.Init(clientSocket);
string adr = conn.GetAddress();
Console.WriteLine("客户端 [" + adr + "] 对象池 id :" + index);
// 开始从连接的socket异步接收数据
}
// 再次调用 BeginAccept 实现循环
serverSocket.BeginAccept(AsyncCb, null);
}
catch (Exception e)
Console.WriteLine("接收数据失败 :" + e.Message);
}
}
private void AsyncRecv(IAsyncResult ar)
{
Conn conn = (Conn)ar.AsyncState;
try
{
// 获取接收的字节数
int count = conn.socket.EndReceive(ar);
// 关闭信号
if(count <= 0)
{
Console.WriteLine("收到 [" + conn.GetAddress() + "] 断开连接");
conn.Close();
return;
}
// 数据处理
string result = System.Text.Encoding.UTF8.GetString(conn.buffer, 0, count);
Console.WriteLine("收到 [" + conn.GetAddress() + "] 数据: " + result);
result = conn.GetAddress() + ":" + result;
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(result);
// 广播
for (int i = 0; i < MAX_CLIENT_NUM; i++)
{
if (conns[i] == null) continue;
if (conns[i].isUse == false) continue;
Console.WriteLine("将消息转播给 :" + conns[i].GetAddress());
conns[i].socket.Send(bytes);
}
conn.socket.BeginReceive(conn.buffer, conn.position, conn.BufferRemainSize, SocketFlags.None, AsyncRecv, conn);
}
catch (Exception)
{
Console.WriteLine("收到 [" + conn.GetAddress() + "] 断开连接");
conn.Close();
}
}
}
}
客户端
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
namespace _2_客户端
{
class Program
{
public static byte[] buffer = new byte[1024];
public static Socket client;
static void Main(string[] args)
{
client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 8888);
client.Connect(ipEndPoint);
Console.WriteLine("客户端连接完成");
client.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, AsyncRecv, null);
while(true)
{
string result = Console.ReadLine();
if(result.ToLower() == "quit")
{
Console.WriteLine("断开连接");
client.Close();
return;
}
byte[] bytes = Encoding.UTF8.GetBytes(result);
client.Send(bytes);
}
}
/// <summary>
/// 接收回调
/// </summary>
/// <param name="ar"></param>
private static void AsyncRecv(IAsyncResult ar)
{
try
{
int count = client.EndReceive(ar);
// 数据处理
string result = Encoding.UTF8.GetString(buffer, 0, count);
Console.WriteLine(result);
client.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, AsyncRecv, null);
}
catch (Exception e)
{
Console.WriteLine("断开连接");
client.Close();
}
}
}