——Java培训、Android培训、iOS培训、.Net培训 期待与您共同交流!——
文章大纲:
1.网络基础
2.TCP通信
3.UDP通信
1. 网络基础
1.1. 网络协议
1.1.1. 网络协议分层
OSI(开放系统互联(Open System Interconnection))模型是国际标准化组织ISO创立的。这是一个理论模型,并无实际产品完全符合OSI模型。制订OSI模型只是为了分析网络通讯方便而引进的一套理论。也为以后制订实用协议或产品打下基础。
OSI模型共分七层:从上至下依次是
图- 1
应用层:指网络操作系统和具体的应用程序,对应WWW服务器、FTP服务器等应用软件
表示层:数据语法的转换、数据的传送等
会话层: 建立起两端之间的会话关系,并负责数据的传送
传输层:负责错误的检查与修复,以确保传送的质量,是TCP工作的地方。
网络层:提供了编址方案,IP协议工作的地方(数据包)
数据链路层:将由物理层传来的未经处理的位数据包装成数据帧
物理层:对应网线、网卡、接口等物理设备(位)
1.1.2. TCP/IP协议栈
- Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,又名网络通讯协议.
- 由网络层的IP协议和传输层的TCP协议组成。TCP/IP 定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求。通俗而言:TCP负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地。而IP是给因特网的每一台电脑规定一个地址。
- TCP/IP协议栈(按TCP/IP参考模型划分),TCP/IP分为4层,不同于OSI,他将OSI中的会话层、表示层规划到应用层:应用层,传输层,IP网络层,网络接口层
1.2. IP层协议
1.2.1. IP协议简介
- IP是英文Internet Protocol(网络之间互连的协议)的缩写,也就是为计算机网络相互连接进行通信而设计的协议。
在因特网中,它是能使连接到网上的所有计算机网络实现相互通信的一套规则,规定了计算机在因特网上进行通信时应当遵守的规则。任何厂家生产的计算机系统,只要遵守 IP协议就可以与因特网互连互通。
1.2.2. 路由器简介
- 路由器(Router)又称网关设备(Gateway)是用于连接多个逻辑上分开的网络,所谓逻辑网络是代表一个单独的网络或者一个子网。当数据从一个子网传输到另一个子网时,可通过路由器的路由功能来完成。因此,路由器具有判断网络地址和选择IP路径的功能,它能在多网络互连环境中,建立灵活的连接,路由器只接受源站或其他路由器的信息,属网络层的一种互联设备。
1.2.3. IP地址及其含义
互联网协议地址(Internet Protocol Address,又译为网际协议地址),缩写为IP地址(IP Address)。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异
IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。
1.3. 传输层协议
1.3.1. TCP协议简介
TCP (Transmission Control Protocol)和UDP(User Datagram Protocol)协议属于传输层协议。其中TCP提供IP环境下的数据可靠传输,它提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。通过面向连接、端到端和可靠的数据包发送。
“面向连接”就是在正式通信前必须要与对方建立起连接。比如你给别人打电话,必须等线路接通了、对方拿起话筒才能相互通话。
1.3.2. UDP协议简介
UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!
1.3.3. 端口号的概念
- 一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等。
- 一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务通过1个IP地址来实现。通过“IP地址+端口号”来区 分不同的服务的。
- 不同的协议间端口不冲突。比如我们可以同时使用UDP打开8088端口的与TCP的8088端口。
1.3.4. TCP协议与UDP协议的区别
- TCP协议需要创建连接,而UDP协议则不需要。
- TCP是可靠的传输协议,而UDP是不可靠的。
- TCP适合传输大量的数据,而UDP适合传输少量数据。
- TCP的速度慢,而UDP的速度快。
1.4. 应用层协议
1.4.1. HTTP协议简介
HTTP协议(HyperText Transfer Protocol,超文本转移协议)是用于从WWW服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。
- HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。 HTTP是一个无状态的协议。
1.4.2. FTP协议简介
FTP 是 TCP/IP 协议组中的协议之一,是英文File Transfer Protocol的缩写。该协议是Internet文件传送的基础,它由一系列规格说明文档组成,目标是提高文件的共享性,提供非直接使用远程计算机,使存储介质对用户透明和可靠高效地传送数据。
1.4.3. SMTP协议简介
SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,是一种提供可靠且有效电子邮件传输的协议。SMTP是建立在FTP文件传输服务上的一种邮件服务,主要用于传输系统之间的邮件信息并提供与来信有关的通知。
2. TCP通信
2.1. Socket原理
2.1.1. Socket简介
socket通常称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。
应用程序通常通过“套接字”向网络发出请求或者应答网络请求。Socket和ServerSocket类库位于java .net包中。ServerSocket用于服务端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。
2.1.2. 获取本地地址和端口号
java.net.Socket为套接字类,其提供了很多方法,其中我们可以通过Socket获取本地的地址以及端口号。
int getLocalPort()
该方法用于获取本地使用的端口号
InetAddress getLocalAddress()
该方法用于获取套接字绑定的本地地址
使用InetAddress获取本地的地址方法:
StringgetCanonicalHostName()
获取此 IP 地址的完全限定域名。
StringgetHostAddress()
返回 IP 地址字符串(以文本表现形式)。
代码如下:
public void testSocket()throws Exception {
Socket socket =newSocket("localhost",8088);
InetAddress add = socket.getLocalAddress();//获取本地地址信息
System.out.println(add.getCanonicalHostName());
System.out.println(add.getHostAddress());
System.out.println(socket.getLocalPort());
}
2.1.3. 获取远端地址和端口号
Socket也提供了获取远端的地址以及端口号的方法:
int getPort()
该方法用于获取远端使用的端口号 。
InetAddress .getInetAddress()
该方法用于获取套接字绑定的远端地址 。
代码如下:
public void testSocket()throws Exception {
Socket socket =newSocket("localhost",8088);
InetAddress inetAdd = socket.getInetAddress();
System.out.println(inetAdd.getCanonicalHostName());
System.out.println(inetAdd.getHostAddress());
System.out.println(socket.getPort());
}
2.1.4. 获取网络输入流和网络输出流
通过Socket获取输入流与输出流,这两个方法是使用Socket通讯的关键方法。封装了TCP协议的Socket是基于流进行通讯的,所以我们在创建了双方连接后,只需要获取相应的输入与输出流即可实现通讯。
InputStream getInputStream()
该方法用于返回此套接字的输入流。
OutputStream .getOutputStream()
该方法用于返回此套接字的输出流。
代码如下:
public void testSocket()throws Exception {
Socket socket =newSocket(“localhost”,8088);
InputStream in= socket.getInputStream();
OutputStream out = socket.getOutputStream();
}
2.1.5. close方法
当使用Socket进行通讯完毕后,要关闭Socket以释放系统资源。
void close()
当关闭了该套接字后也会同时关闭由此获取的输入流与输出流。
2.2. Socket通讯模型
2.2.1. Server端ServerSocket监听
java.net.ServerSocket是运行于服务端应用程序中。通常创建ServerSocket需要指定服务端口号,之后监听Socket的连接。监听方法为:
Socket accept()
该方法是一个阻塞方法,直到一个客户端通过Socket连接后,accept会封装一个Socket,该Socket封装与表示该客户端的有关的信息。通过这个Socket与该客户端进行通信。
代码如下:
…
//创建ServerSocket并申请服务端口8088
ServerSocket server =newServerSocket(8088);
/*方法会产生阻塞,直到某个Socket连接,并返回请求连接的Socket*/
Socket socket = server.accept();
…
2.2.2. Client端Socket连接
通过上一节我们已经知道,当服务端ServerSocket调用accept方法阻塞等待客户端连接后,我们可以通过在客户端应用程序中创建Socket来向服务端发起连接。
- 需要注意的是,创建Socket的同时就发起连接,若连接异常会抛出异常。 我们通常创建Socket时会传入服务端的地址以及端口号。
代码如下:
//参数1:服务端的IP地址,参数2:服务端的服务端口
Socket socket =newSocket(“localhost”,8088);
…
2.2.3. C-S端通信模型
C-S的全称为(Client-Server):客户端-服务器端
客户端与服务端通信模型如下:
图- 2
服务端创建ServerSocket
通过调用ServerSocket的accept方法监听客户端的连接
客户端创建Socket并指定服务端的地址以及端口来建立与服务端的连接
当服务端accept发现客户端连接后,获取对应该客户端的Socket
双方通过Socket分别获取对应的输入与输出流进行数据通讯
通讯结束后关闭连接。
代码如下:
/**
* Server端应用程序
*/
publicclass Server {
publicstatic void main(String[] args){
ServerSocket server =null;
try{
//创建ServerSocket并申请服务端口为8088
server =newServerSocket(8088);
//侦听客户端的连接
Socket socket = server.accept();
//客户端连接后,通过该Socket与客户端交互
//获取输入流,用于读取客户端发送过来的消息
InputStream in= socket.getInputStream();
BufferedReader reader
=newBufferedReader(
newInputStreamReader(
in,"UTF-8"
)
);
//获取输出流,用于向该客户端发送消息
OutputStream out = socket.getOutputStream();
PrintWriter writer
=newPrintWriter(
newOutputStreamWriter(
out,"UTF-8"
),true
);
//读取客户端发送的消息
String message = reader.readLine();
System.out.println("客户端说:"+message);
//向客户端发送消息
writer.println("你好客户端!");
}catch(Exception e){
e.printStackTrace();
}finally{
if(server !=null){
try{
server.close();
}catch(IOException e){
}
}
}
}
}
/**
* Client端应用程序
*/
publicclass Client {
publicstatic void main(String[] args){
Socket socket =null;
try{
socket =newSocket("localhost",8088);
//获取输入流,用于读取来自服务端的消息
InputStream in= socket.getInputStream();
BufferedReader reader
=newBufferedReader(
newInputStreamReader(
in,"UTF-8"
)
);
//获取输出流,用于向服务端发送消息
OutputStream out
= socket.getOutputStream();
OutputStreamWriter osw
=newOutputStreamWriter(out,"UTF-8");
PrintWriter writer
=newPrintWriter(osw,true);
//向服务端发送一个字符串
writer.println("你好服务器!");
//读取来自客户端发送的消息
String message = reader.readLine();
System.out.println("服务器说:"+message);
}catch(Exception e){
e.printStackTrace();
}finally{
try{
if(socket !=null){
//关闭Socket
socket.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
2.2.4. Server端多线程模型
通过上一节我们已经知道了如何使用ServerSocket与Socket进行通讯了,但是这里存在着一个问题,就是只能“p2p”点对点。一个服务端对一个客户端。若我们想让一个服务端可以同时支持多个客户端应该怎么做呢?这时我们需要分析之前的代码。我们可以看到,当服务端的ServerSocket通过accept方法侦听到一个客户端Socket连接后,就获取该Socket并与该客户端通过流进行双方的通讯了,这里的问题在于,只有不断的调用accept方法,我们才能侦听到不同客户端的连接。但是若我们循环侦听客户端的连接,又无暇顾及与连接上的客户端交互,这时我们需要做的事情就是并发。我们可以创建一个线程类ClientHandler,并将于客户端交互的工作全部委托线程来处理。这样我们就可以在当一个客户端连接后,启动一个线程来负责与客户端交互,而我们也可以循环侦听客户端的连接了。
我们需要对服务端的代码进行修改:
/**
* Server端应用程序*
*/
publicclass Server {
publicstatic void main(String[] args){
ServerSocket server =null;
try{
//创建ServerSocket并申请服务端口为8088
server =newServerSocket(8088);
while(true){
//循环侦听客户端的连接
Socket socket = server.accept();
//当一个客户端连接后,启动线程来处理该客户端的交互
newClientHandler(socket).start();
}
}catch(Exception e){
e.printStackTrace();
}finally{
if(server !=null){
try{
server.close();
}catch(IOException e){
}
}
}
}
}
/**
* 线程类
* 该线程的作用是并发与客户端进行交互
* 这里的代码就是原来在Server中客户端连接后交互的代码
*/
class ClientHandler extends Thread{
private Socket socket;
publicClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
try{
//获取输入流,用于读取客户端发送过来的消息
InputStream in= socket.getInputStream();
BufferedReader reader
=newBufferedReader(
newInputStreamReader(
in,"UTF-8"
)
);
//获取输出流,用于向该客户端发送消息
OutputStream out = socket.getOutputStream();
PrintWriter writer
=newPrintWriter(
newOutputStreamWriter(
out,"UTF-8"
),true
);
//读取客户端发送的消息
String message = reader.readLine();
System.out.println("客户端说:"+message);
//向客户端发送消息
writer.println("你好客户端!");
}catch(Exception e){
e.printStackTrace();
}
}
}
经过上面的改动,我们再次启动服务端,这个时候我们会发现,我们启动若干客户端都可以被服务器所接受并进行交互了。
3. UDP通信
3.1. DatagramPacket
3.1.1. 创建接收包
DatagramPacket:UDP数据报基于IP建立的,每台主机有65536个端口号可以使用。数据报中字节数限制为65536-8 。包含8字节的头信息。
构造接收包:
DatagramPacket(byte[] buf, int length)
将数据包中Length长的数据装进Buf数组。
DatagramPacket(byte[] buf, int offset, int length)
将数据包中从Offset开始、Length长的数据装进Buf数组。
3.1.2. 创建发送包
构造发送包:
DatagramPacket(byte[] buf, int length, InetAddress clientAddress, int clientPort)
从Buf数组中,取出Length长的数据创建数据包对象,目标是clientAddress地址,clientPort端口,通常用来发送数据给客户端。
DatagramPacket(byte[] buf, int offset, int length, InetAddress clientAddress, int clientPort)
从Buf数组中,取出Offset开始的、Length长的数据创建数据包对象,目标是clientAddress地址,clientPort端口,通常用来发送数据给客户端。
3.2. DatagramSocket
3.2.1. 服务端接收
DatagramSocke用于接收和发送UDP的Socket实例 。
DatagramSocket(int port)
创建实例,并固定监听Port端口的报文。通常用于服务端。
其中方法:
receive(DatagramPacket d)
接收数据报文到d中。receive方法产生 “阻塞”。会一直等待知道有数据被读取到。
3.2.2. 客户端发送
无参的构造方法DatagramSocket()通常用于客户端编程,它并没有特定监听的端口,仅仅使用一个临时的。程序会让操作系统分配一个可用的端口。
其中方法:
send(DatagramPacket dp)
该方法用于发送报文dp到目的地。
代码如下:
/**
* Server端程序
*/
publicclass Server {
publicstatic void main(String[] args){
DatagramSocket socket =null;
try{
socket =newDatagramSocket(8088);//申请8088端口
byte[] data =new byte[1024];
DatagramPacket packet
=newDatagramPacket(data, data.length);//创建接收包
socket.receive(packet);//会产生阻塞,读取发送过来的数据
String str =newString(packet.getData(),0,packet.getLength());//从包中取数据
System.out.println(str);
}catch(Exception e){
e.printStackTrace();
}finally{
if(socket !=null){
socket.close();//关闭释放资源
}
}
}
}
/**
* Client端程序
*/
publicclass Client {
publicstatic void main(String[] args){
DatagramSocket socket =null;
try{
socket =newDatagramSocket();//创建Socket
byte[] data ="你好服务器!".getBytes();
DatagramPacket packet =newDatagramPacket(
data,
data.length,
InetAddress.getByName("localhost"),
8088
);//创建发送包
socket.send(packet);//发送数据
}catch(Exception e){
e.printStackTrace();
}finally{
if(socket !=null){
socket.close();//关闭以释放资源
}
}
}
}
3.3. UDP穿透
3.3.1. UDP穿透原理
在讲解UDP穿透之前,我们先来介绍一些相关的知识。
NAT(Network Address Translators),网络地址转换:网络地址转换是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够使IP地址重用。NAT分为两大类,基本的NAT和NAPT(Network Address/Port Translator)。 最开始NAT是运行在路由器上的一个功能模块。 基本的NAT实现的功能很简单,在子网内使用一个保留的IP子网段,这些IP对外是不可见的。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP然后发送出去。(基本的NAT会改变IP包中的原IP地址,但是不会改变IP包中的端口)
假设一台在NAT211.133.111.022后的192.168.1.77:8000要向NAT211.134.222.123后的192.168.1.88:9000发送数据,假设你向211.134.222.123这个IP地址的9000端口直接发送数据包,则数据包在到达NAT211.134.222.123之后,会被当做无效非法的数据包被丢弃,NAT在此时相当于一个防火墙,会对没有建立起有效SESSION的数据包进行拒绝转递。当然,你也不能直接用内网地址192.168.1.88进行发送数据包,因为这样是找自己内网的其他机器。 凡是经过NAT发出去的数据包,都会通过一定的端口转换(而非使用原端口)再发出去,也就是说内网和外网之间的通信不是直接由内网机器与外网NAT进行,而是利用内网对外网的NAT建立起SESSION与外网NAT的SESSION进行。 根据SESSION的不同,NAT主要分成两种:SymmetricNAPT以及CONE NAPT。简单的说,Symmetric NAPT是属于动态端口映射的NAT,而CONE NAPT是属于静态端口映射的NAT。而市场上目前大多属于后者,CONE的英文意思锥,意思就是一个端口可以对外部多台NAT设备通信。这个也正是我们做点对点穿透的基本,是我们所希望的,否则现在的大部分点对点软件将无法正常使用。
像上面的例子,NAT211.133.111.022和NAT211.134.222.123之间需要进行通信,但开始不能直接就发数据包,我们需要一个中间人,这个就是外部索引服务器(我们假设是211.135.134.178:7000),当NAT211.133.111.022向211.135.134.178:7000发送数据包,211.135.134.178:7000是可以正常接收到数据,因为它是属于对外型开放的服务端口。当211.135.134.178:7000收到数据包后可以获知NAT211.133.111.022对外通信的临时SESSION信息(这个临时的端口,假设是6000会过期,具体的时间不同,索引服务器此时应将此信息保存起来。而同时,NAT211.134.222.123也在时刻向索引服务器发送心跳包,索引服务器就向NAT211.134.222.123发送一个通知,让它向NAT211.133.111.022:6000发送探测包(这个数据包最好多发几个),NAT211.133.111.022在收到通知包之后再向索引服务器发送反馈包,说明自己已经向NAT211.133.111.022:6000发送了探测包,索引服务器在接收到反馈包之后再向NAT211.133.111.022转发反馈包,NAT211.133.111.022在接收到数据包之后再向原本要请求的NAT211.134.222.123发送数据包,此时连接已经打通,实现穿透,NAT211.134.222.123会将信息转发给192.168.1.88的9000端口。
3.3.2. UDP穿透参考实例
以上一节的案例说明具体过程:
我们要从一个内网中的机器A连接另一个内网机器B。
假设:
A的内网IP和端口为: 192.168.1.77:8000
B的内网IP和端口为: 192.168.1.88:9000
A所连接的路由器NA的公网IP为: 211.133.111.022
B所连接的路由器NB的公网IP为: 211.134.222.123
需要借助的服务器C的公网IP和端口为: 211.135.134.178:7000
那么我们实现穿透需要做以下操作:
1:首先A请求服务器C,这时A的路由器会做端口映射,记录来自服务器的消息是允许进入并转发给内网A的。
图- 3
2:同样的,B也连接服务器C
图- 4
3:若A想与B连接,需要先让服务器C将B的地址发送给A,使得A主动向B发送消息。如此一来,A的路由器NA会记录来自B所在的公网IP的消息允许进入。但是,实际上这次A向B所在的路由器NB发送的消息会被NB当做无效信息而被屏蔽,不过没有关系。我们的目的是告诉NA来自NB的消息是允许进入的。
图- 5
4:之后A请求服务器C,使服务器C通知B向A所在的路由器NA发送消息以连接A。
图- 6
图- 7
6:同时在步骤5发送消息后,NB也允许来自NA的消息了。从而实现了NA的内网机器A与NB的内网机器B的双向连通,可以互发信息了。实现了UDP穿透。
图- 8
文章到此结束,谢谢阅读,如有不足之处请愿与你共同商讨。
版权声明:本文为博主原创文章,未经博主允许不得转载。