——Java培训、Android培训、iOS培训、.Net培训 期待与您共同交流!——
文章大纲:
1.网络基础
2.TCP通信
3.UDP通信
OSI(开放系统互联(Open System Interconnection))模型是国际标准化组织ISO创立的。这是一个理论模型,并无实际产品完全符合OSI模型。制订OSI模型只是为了分析网络通讯方便而引进的一套理论。也为以后制订实用协议或产品打下基础。
OSI模型共分七层:从上至下依次是
图- 1
应用层:指网络操作系统和具体的应用程序,对应WWW服务器、FTP服务器等应用软件
表示层:数据语法的转换、数据的传送等
会话层: 建立起两端之间的会话关系,并负责数据的传送
传输层:负责错误的检查与修复,以确保传送的质量,是TCP工作的地方。
网络层:提供了编址方案,IP协议工作的地方(数据包)
数据链路层:将由物理层传来的未经处理的位数据包装成数据帧
物理层:对应网线、网卡、接口等物理设备(位)
在因特网中,它是能使连接到网上的所有计算机网络实现相互通信的一套规则,规定了计算机在因特网上进行通信时应当遵守的规则。任何厂家生产的计算机系统,只要遵守 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)。
TCP (Transmission Control Protocol)和UDP(User Datagram Protocol)协议属于传输层协议。其中TCP提供IP环境下的数据可靠传输,它提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。通过面向连接、端到端和可靠的数据包发送。
“面向连接”就是在正式通信前必须要与对方建立起连接。比如你给别人打电话,必须等线路接通了、对方拿起话筒才能相互通话。
UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!
HTTP协议(HyperText Transfer Protocol,超文本转移协议)是用于从WWW服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。
FTP 是 TCP/IP 协议组中的协议之一,是英文File Transfer Protocol的缩写。该协议是Internet文件传送的基础,它由一系列规格说明文档组成,目标是提高文件的共享性,提供非直接使用远程计算机,使存储介质对用户透明和可靠高效地传送数据。
SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,是一种提供可靠且有效电子邮件传输的协议。SMTP是建立在FTP文件传输服务上的一种邮件服务,主要用于传输系统之间的邮件信息并提供与来信有关的通知。
socket通常称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。
应用程序通常通过“套接字”向网络发出请求或者应答网络请求。Socket和ServerSocket类库位于java .net包中。ServerSocket用于服务端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。
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());
}
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());
}
通过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();
}
当使用Socket进行通讯完毕后,要关闭Socket以释放系统资源。
void close()
当关闭了该套接字后也会同时关闭由此获取的输入流与输出流。
java.net.ServerSocket是运行于服务端应用程序中。通常创建ServerSocket需要指定服务端口号,之后监听Socket的连接。监听方法为:
Socket accept()
该方法是一个阻塞方法,直到一个客户端通过Socket连接后,accept会封装一个Socket,该Socket封装与表示该客户端的有关的信息。通过这个Socket与该客户端进行通信。
代码如下:
…
//创建ServerSocket并申请服务端口8088
ServerSocket server =newServerSocket(8088);
/*方法会产生阻塞,直到某个Socket连接,并返回请求连接的Socket*/
Socket socket = server.accept();
…
通过上一节我们已经知道,当服务端ServerSocket调用accept方法阻塞等待客户端连接后,我们可以通过在客户端应用程序中创建Socket来向服务端发起连接。
//参数1:服务端的IP地址,参数2:服务端的服务端口
Socket socket =newSocket(“localhost”,8088);
…
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();
}
}
}
}
通过上一节我们已经知道了如何使用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();
}
}
}
经过上面的改动,我们再次启动服务端,这个时候我们会发现,我们启动若干客户端都可以被服务器所接受并进行交互了。
DatagramPacket:UDP数据报基于IP建立的,每台主机有65536个端口号可以使用。数据报中字节数限制为65536-8 。包含8字节的头信息。
构造接收包:
DatagramPacket(byte[] buf, int length)
将数据包中Length长的数据装进Buf数组。
DatagramPacket(byte[] buf, int offset, int length)
将数据包中从Offset开始、Length长的数据装进Buf数组。
构造发送包:
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端口,通常用来发送数据给客户端。
DatagramSocke用于接收和发送UDP的Socket实例 。
DatagramSocket(int port)
创建实例,并固定监听Port端口的报文。通常用于服务端。
其中方法:
receive(DatagramPacket d)
接收数据报文到d中。receive方法产生 “阻塞”。会一直等待知道有数据被读取到。
无参的构造方法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();//关闭以释放资源
}
}
}
}
在讲解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端口。
以上一节的案例说明具体过程:
我们要从一个内网中的机器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
文章到此结束,谢谢阅读,如有不足之处请愿与你共同商讨。
版权声明:本文为博主原创文章,未经博主允许不得转载。
原文地址:http://blog.csdn.net/u010111458/article/details/47135663