Java 网络
网络基础知识
1、计算机网络
计算机网络的概念:把分布在不同地理区域的计算机通过专门的通信线路连接在一起形成的规模庞大的、功能强大的网络系统,一个非常著名的网络万维网( World Wide Web , 即www )。
计算机网络的作用:
- 资源共享
- 信息传输和集中处理
- 均衡负荷与分布处理
- 综合信息服务
计算机网络的分类:
- 按照网络结构划分
星型网络、总线型网络、环线型网络、树形网络、星型环线网络
- 按照介质来分
双绞线网络、同轴电缆网络、光纤网、无线网、电力线网
- 按照规模来划分
局域网( LAN )、城域网( MAN )、广域网( WAN )
2、通信网络协议
通信网络协议的概念:网络通信协议是一种网络通用语言,为连接不同操作系统和不同硬件体系结构的互联网络引提供通信支持,是一种网络通用语言。
常见协议:
- OSI
OSI是Open System Interconnect的缩写,意为开放式系统互联。
国际标准组织(国际标准化组织)制定了OSI模型,这个模型把网络通信的工作分为7层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
- TCP/IP
IP是英文Internet Protocol(网络之间互连的协议)的缩写中,文简称为“网协”,是为计算机网络相互连接进行通信而设计的协议。
TCP:Transmission
Control Protocol 传输控制协议,是一种面向连接(连接导向)的、可靠的、基于字节流的运输层(Transport
layer)通信协议,由IETF的RFC 793说明。 在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能。
UDP是同一层内另一个重要的传输协议。
3、IP地址
IP 地址是根据IP 协议为某个通信实体分配的地址
- 这个通信实体,有可能是计算机、打印机、路由器的某个端口。
IP地址是数字型的,是个32 位( 32bit ) 的整数
- 通常为了方便记忆,把这32 位分成4 个部分,每个部分存放8 位。
- 因此我们实际上看到的ip 地址形式为:192.168.95.27。
NIC 负责全球的IP 地址的规划、管理
- NIC : Internet Network Information Center。
- NIC 下属Inter NIC 、APNIC、PIPE 机构负责美国及其他地区的IP 地址分配。
- APNIC 总部在日本东京大学,负责亚太地区的IP地址分配。
IP地址被分成A、B、C、D、E 五类
- A类: 10.0.0.0 ~ 10.255.255.255
- B类: 172.16.0.0 ~ 172.31.255.255
- C类: 192.168.0.0 ~ 192.168.255.255
一个特殊的IP 地址
- 127.0.0.1 : 代表当前计算机本身。
查询IP地址的命令:
- Windows : ipconfig /all
- Unix / Linux / Mac OS : ifconfig -a
IP v4 :
- 采用 32 bit 的数字表示一个地址 -------> java.net.Inet4Address
- 通常看到的 192.168.100.100 形式是将 32bit 拆分成 4 各部分,每个部分 8bit
- 不同的部分之间 用 圆点 进行分隔
IP v6 :
- 采用 128bit 的数字表示一个地址 -------> java.net.Inet6Address
- 通常看到的 FEC0:0:0:ffff::1
- 每个部分由 4 位 十六进制 表示的 整数 组成,比如 FEC0 , 每部分占 16 bit
- 不同的部分之间 用 冒号 进行分隔 ,( 分成 8 个部分 )
- 如果 某个部分 的 4 个十六进制数字都是 0 ,可以只写一个 ,比如 0000 可以写成 0
- FEC0:0:0:ffff::1 ------> FEC0:0000:0000:FFFF:0000:0000:0000:0001
4、端口
端口的作用:一个IP 地址可以唯一地确定一个通信实体,一个通信实体上可以有多个程序提供服务比如一台服务器上可以有多个DBMS,如MySQL 、DB2、Oracle,为了区别同一个通信实体上的不同服务(程序),还要使用端口。
端口的概念:
- 端口是一个16 bit的整数,用于表示数据交给哪个通信程序处理。
- 端口是应用程序与外界交流的出入口,是种抽象的软件结构。
端口的分类:
- 不同的应用程序处理不同端口上的数据同一个计算机上,不允许有两个以上程序使用同一个端口。
- 我们自己在使用端口时,尽量使用1024 以上的端口,同时还要注意避开已经被已有的服务占用的端口。
- 端口的范围从0 到65535 ,被分成三类
公认端口: 从0 到1023
他们紧密绑定一些特定服务,如80 端口、23端口、21端口等等
注册端口: 从1024 到49151
松散地绑定一些服务,比如
» Oracle 数据库默认的端口是1521
» MySQL 数据库默认的端口是3306
» Tomcat 默认的端口是8080
动态或私有端口: 从49152 到65535
应用程序使用的动态端口,应用程序一般不会主动去使用这些端口
InetAddress
1、InetAddress 用来代表IP 地址
它有两个子类
- Inet4Address : 对应IPv4 地址
- Inet6Address : 对应IPv6 地址
没有构造,通过以下静态方法获取该类的实例
- getByName( String hostName ) :根据指定主机名称得到对应的InetAddress 实例
- getByAddress( byte[] address ) :根据指定的IP 地址获取对应的InetAddress 实例
常用方法
- String getCanonicalHostName()获取此IP 地址的完全限定域名
- String getHostAddress()返回IP 地址字符串(以文本表现形式)
- String getHostName()获取此IP 地址的主机名
- static InetAddress getLocalHost()返回本地主机对应的InetAddress 实例
- boolean isReachable(int timeout)测试是否可以达到该地址
InetAddress测试案例一:
package ecut.network;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
public class InetAddressTest1 {
public static void main(String[] args) throws UnknownHostException {
// 构造方法私有,通过静态方法获得 当前主机的 一个 IP 地址 对应的 InetAddress 实例
InetAddress local = InetAddress.getLocalHost() ;
System.out.println( local );
System.out.println( "主机名称: " + local.getHostName() );
System.out.println( "主机地址: " + local.getHostAddress() );
byte[] address = local.getAddress(); // 以 byte 数组形式表示的 IP 地址
System.out.println( address.length );
System.out.println( Arrays.toString( address ) );
}
}
InetAddress测试案例二:
package ecut.network; import java.net.InetAddress; import java.net.UnknownHostException; public class InetAddressTest2 { public static void main(String[] args) throws UnknownHostException { System.out.println( (byte)172 ); // byte[] address = { 1 , 0 , 0 , 100 }; byte[] address = { (byte)172 , 26 , 28 , 55 }; InetAddress remote = InetAddress.getByAddress( address ); System.out.println( remote.getHostAddress() ); } }
InetAddress测试案例三:
package ecut.network; import java.net.InetAddress; import java.net.UnknownHostException; public class InetAddressTest3 { public static void main(String[] args) throws UnknownHostException { InetAddress ia = InetAddress.getByName( "V-AIMEIZHENGX" ); System.out.println( ia.getHostAddress() ); System.out.println( "~~~~~~~~~~~~~~~~~~" ); InetAddress[] addresses = InetAddress.getAllByName( "V-AIMEIZHENGX" ); for( int i = 0 , n = addresses.length ; i < n ;i++){ InetAddress a = addresses[ i ] ; System.out.println( a.getHostAddress() ); } } }
InetSocketAddress
1、java.net.InetSocketAddress 类型的实例表示 ( IP + Port )
构造方法
- InetSocketAddress(InetAddress addr, int port)根据 IP 地址和端口号创建套接字地址。
- InetSocketAddress(String hostname, int port)根据主机名和端口号创建套接字地址。
InetSocketAddress 测试案例一:
package ecut.network; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; public class InetSocketAddressTest1 { public static void main(String[] args) throws UnknownHostException { InetAddress address = InetAddress.getByName( "1.0.0.2" );//传ip地址和主机名都可以 System.out.println( address.getHostName() ); int port = 9527 ; InetSocketAddress isa = new InetSocketAddress( address , port ); System.out.println( isa ); System.out.println( isa.getHostName() ); System.out.println( isa.getAddress().getHostAddress() ); System.out.println( isa.getPort() ); } }
InetSocketAddress 测试案例二:
package ecut.network; import java.net.InetSocketAddress; import java.net.UnknownHostException; public class InetSocketAddressTest2 { public static void main(String[] args) throws UnknownHostException { String hostname = "1.0.0.2";//传ip地址和主机名都可以 int port = 9527 ; InetSocketAddress isa = new InetSocketAddress( hostname , port ); System.out.println( isa ); System.out.println( isa.getHostName() ); System.out.println( isa.getAddress().getHostAddress() ); System.out.println( isa.getPort() ); } }
URL
1、URL 对象代表Uniform Resource Locator 统一资源定位符(协议://主机:端口/资源路径和名称)
它是指向互联网"资源"的指针
- 这里的资源可以是简单的文件或目录。
- 也可以是更为复杂的对象(比如数据库对象)。
URL 通常由协议名、主机名、端口和资源组成,格式如下:
- protocol : // host : port / resourceName。
- 常见的URL 如:http://www.baidu.com:80/index.html。
- ftp://ftp.baidu.com:xx/金刚.mkv 、 jdbc:mysql://localhost:3306/ecut?useUnicode=true&characterEncoding=utf8、jdbc:oracle:thin:@localhost:1521:ecut
2、URL 类的构造
该类有很多重载的构造
- 但不外乎就是指定协议名、主机名、端口、资源名等参数
可以根据已有的URL 创建全新的URL 对象
- URL(URL context, String spec)
- URL(URL context, String spec, URLStreamHandler handler)
3、URL 类中常用方法
String getFile() 获取此URL 的文件名。
String getHost() 获取此URL 的主机名(如果适用)。
String getPath() 获取此URL 的路径部分。
int getPort() 获取此URL 的端口号。
String getProtocol() 获取此URL 的协议名称。
String getQuery() 获取此URL 的查询部分。
URLConnection openConnection()返回一个URLConnection 对象,它表示到URL 所引用的远程对象的连接。
InputStream openStream()打开到此URL 的连接并返回一个用于从该连接读入的InputStream。
URL 测试案例:
package ecut.network; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; public class URLTest { public static void main(String[] args) throws IOException { // http://www.baidu.com:80/index.html //MalformedURLException 如果指定了未知协议。 URL u = new URL( "http", "www.baidu.com", 80, "/customer/account/change/password.do?date=20170506" ); System.out.println( u ); System.out.println( "协议: " + u.getProtocol() ); System.out.println( "主机: " + u.getHost() ); System.out.println( "端口: " + u.getPort() ); System.out.println( "File :" + u.getFile() ); System.out.println( "Path : " + u.getPath() ); System.out.println( "Query : " + u.getQuery() ); System.out.println( "~~~~~~~~~~~~~~~~~~~~~~~~~" ); InputStream in = u.openStream() ; System.out.println( in ); } }
运行结果如下:
http://www.baidu.com:80/customer/account/change/password.do?date=20170506 协议: http 主机: www.baidu.com 端口: 80 File :/customer/account/change/password.do?date=20170506 Path : /customer/account/change/password.do Query : date=20170506 ~~~~~~~~~~~~~~~~~~~~~~~~~ sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@3e3abc88
TCP 协议
1、TCP 协议
使用IP 协议可以将一个消息从一个通信主体发送到另一个通信主体,消息在传输过程中被分割成一个一个的小数据包。
虽然解决了数据发送和接受问题,但是不能解决数据分组在传输过程中可能出现的问题。
为了解决以上问题,需要提供一整套保障无差错通信的措施或协议,这就是目前使用广泛的TCP 协议。
TCP 协议被称作端对端协议,通过该协议建立了两个通信实体之间用于发送和收取数据的虚拟链路。
2、TCP 协议的可靠性
TCP 协议负责收集被分割的数据分组,并将其按照适当的次序发送;对方在接受到数据分组后再将其正确还原。
为了保证数据包传送中准确无误,TCP 协议使用重发机制。
- 通信实体A发送一个消息给通信实体B后,会等待通信实体B返回的确认信息,如果A没有收到B的确认信息,则A会再次发送该消息
- 这种重发机制,保证了数据传输的可靠性和完整性。
ServerSocket
1、使用ServerSocket 创建TCP 服务器端
ServerSocket 类的对象用于监听来自其它通信实体的连接
- 该类的accept() 方法用于监听来自外部的连接。
- 如果没有任何通信实体连接该ServerSocket 对象,它将永远等待下去。
获得ServerSocket 对象
- 该类中提供了许多重载的构造方法,可以用于实例化ServerSocket。
- 一般而言需要指定IP 和port。
Socket
1、获得Socket 进行通信
ServerSocket 实例只负责监听来自其它通信实体的连接
- 如果accept() 方法监听到来自外部的通信实体的连接,它将返回一个Socket。
- Socket accept() : 返回监听到的连接对应的Socket 的实例。
使用Socket 类的实例才能实现通信,该类中有两个非常重要的方法
- InputStream getInputStream()返回当前的Socket 对应的输入流,让程序通过该流从Socket 中读取数据。
- OutputStream getOutputStream()返回当前的Socket 对应的输出流,让程序通过该流向Socket 中写入数据。
使用Socket 创建另外一个通信实体
- 构造一个Socket 实例,让它去连接前面建好的ServerSocket
- Socket 类构造有多种重载形式,但一般需要指定连接哪个主机或哪个ip,另外还要指定端口号,比如:Socket s = new Socket("127.0.0.1" , 8888 );
服务端监听一次测试案例:
package ecut.network; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; public class ServerV1 { public static void main(String[] args) throws Exception { // 创建一个 ServerSocket 对象,用来对外提供服务 ServerSocket server = new ServerSocket(); System.out.println( server ); System.out.println( server.isBound() ); SocketAddress endpoint = new InetSocketAddress( "10.10.12.72", 5555 ); server.bind( endpoint ); System.out.println( "server ip : " + server.getInetAddress().getHostAddress()); System.out.println( "server port : " +server.getLocalPort() ); // 监听来自客户端的连接 ( 会阻塞当前线程 ) Socket socketFromClient = server.accept(); System.out.println( "socket : " + socketFromClient ); System.out.println( "server ip ( local ) : " + socketFromClient.getLocalAddress().getHostAddress() ); System.out.println( "server port ( local ) : " + socketFromClient.getLocalPort() ); InetSocketAddress sa = (InetSocketAddress) socketFromClient.getRemoteSocketAddress(); System.out.println( "client ip : " + sa.getHostString() ); System.out.println( "client port : " + sa.getPort() ); server.close(); } }
package ecut.network; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; public class ClientV1 { public static void main(String[] args) throws IOException { // 创建一个 Socket 对象,充当客户端程序 Socket client = new Socket(); SocketAddress bindpoint = new InetSocketAddress( "10.10.12.72", 3333 ); // 为客户端绑定本地的IP地址和端口 client.bind( bindpoint ); System.out.println( client ); System.out.println( client.getLocalAddress().getHostAddress() ); System.out.println( client.getLocalPort() ); SocketAddress remote = new InetSocketAddress( "10.10.12.72", 5555 ); // 连接 "远程" 服务 client.connect( remote ); System.out.println( client ); } }
运行结果如下:
ServerSocket[unbound] false server ip : 10.10.12.72 server port : 5555 socket : Socket[addr=/10.10.12.72,port=3333,localport=5555] server ip ( local ) : 10.10.12.72 server port ( local ) : 5555 client ip : 10.10.12.72 client port : 3333
Socket[unconnected] 10.10.12.72 3333 Socket[addr=/10.10.12.72,port=5555,localport=3333]
服务端持续监听测试案例:
package ecut.network; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; public class ServerV2 { @SuppressWarnings("resource") public static void main(String[] args) throws Exception { // 创建一个 ServerSocket 对象 ServerSocket server = new ServerSocket(); SocketAddress endpoint = new InetSocketAddress( "10.10.12.72", 6666 ); server.bind( endpoint ); while( true ) { // 监听来自客户端的连接 ( 会阻塞当前线程 ) Socket socketFromClient = server.accept(); InetSocketAddress sa = (InetSocketAddress) socketFromClient.getRemoteSocketAddress(); System.out.print( "client ip : " + sa.getHostString() ); System.out.println( " , client port : " + sa.getPort() ); } } }
package ecut.network; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; public class ClientV2 { public static void main(String[] args) throws IOException { // 创建一个 Socket 对象,充当客户端程序 Socket client = new Socket();//默认指定为当地的IP地址,将一个空闲的端口号用于使用 SocketAddress remote = new InetSocketAddress( "10.10.12.72", 6666 ); // 连接 "远程" 服务 client.connect( remote ); System.out.println( client ); } }
运行结果如下:
Socket[addr=/10.10.12.72,port=6666,localport=50688]
Socket[addr=/10.10.12.72,port=6666,localport=50699]
client ip : 10.10.12.72 , client port : 50688
client ip : 10.10.12.72 , client port : 50699
服务端持续监听测试案例:
package ecut.network; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; public class ServerV3 { @SuppressWarnings("resource") public static void main(String[] args) throws IOException { // 创建一个 ServerSocket 对象 System.out.println("服务器启动中..."); ServerSocket server = new ServerSocket(); SocketAddress endpoint = new InetSocketAddress("10.10.12.72", 6666); System.out.println("服务器正在初始化..."); server.bind(endpoint); System.out.println("服务器初始化完成,准备对外提供服务"); while (true) { System.out.println("服务器正在监听来自客户端的连接..."); // ( 会阻塞当前线程 ) Socket socket = server.accept(); try { // 获得 可以从 当前监听到的 客户端连接 中 读取数据的 字节输入流 InputStream in = socket.getInputStream(); Reader reader = new InputStreamReader(in); BufferedReader br = new BufferedReader(reader); while (true) { String s = br.readLine(); System.out.println("来自客户端的信息: [ " + s + " ]"); if ("byebyebye".equalsIgnoreCase(s)) { break; } } } catch (IOException e) { if (e instanceof SocketException) { SocketException se = (SocketException) e; String message = se.getMessage(); System.out.print("message"+message); if ("Connection reset".equalsIgnoreCase(message)) { System.out.println("客户端断开"); } else { se.printStackTrace(); } } else { e.printStackTrace(); } } } } }
package ecut.network; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.util.Scanner; public class ClientV3 { public static void main(String[] args) throws IOException { final Scanner scanner = new Scanner( System.in ); // 1、创建一个可以连接远程服务器的一个 Socket 对象 ( 充当客户端 ) Socket client = new Socket(); SocketAddress remote = new InetSocketAddress( "10.10.12.72", 6666 ); // 2、连接 "远程" 服务器上的指定程序 client.connect( remote ); // 3、获得可以向服务器输出数据的 字节输出流 OutputStream out = client.getOutputStream(); PrintStream ps = new PrintStream( out ); while( true ){ System.out.println( "请输入你要向服务器发送的信息:" ); String s = scanner.nextLine(); ps.println( s ); if( "byebyebye".equalsIgnoreCase( s ) ){ break ; } } // end client.close(); scanner.close(); } }
实现群聊测试案例:
package ecut.network; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.Reader; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.util.HashMap; import java.util.Map; public class ServerV4 { private static final Map<String,Socket> SOCKETS = new HashMap<>() ; @SuppressWarnings("resource") public static void main(String[] args) throws IOException { // 创建一个 ServerSocket 对象 System.out.println( "服务器启动中..." ); ServerSocket server = new ServerSocket(); SocketAddress endpoint = new InetSocketAddress( "192.168.0.106", 6666 ); System.out.println( "服务器正在初始化..." ); server.bind( endpoint ); System.out.println( "服务器初始化完成,准备对外提供服务" ); while( true ) { System.out.println( "服务器正在监听来自客户端的连接..."); // ( 会阻塞当前线程 ) Socket socket = server.accept(); InetSocketAddress remote = (InetSocketAddress) socket.getRemoteSocketAddress(); String name = remote.getHostString() + ":" + remote.getPort() ; // 将当前监听到的 客户端 name ( ip:port ) 跟 响应的 Socket 对象建立 "映射" SOCKETS.put( name , socket ) ; System.out.println( "创建为[ " + name + " ]提供服务的线程" ); ServerThread t = new ServerThread( socket , name ) ; System.out.println( "启动为[ " + name + " ]提供服务的线程" ); t.start(); } } public static class ServerThread extends Thread { private String name ; private Socket socket ; public ServerThread(Socket socket , String name ) { super( name ); this.name = name ; if( socket == null ) { throw new RuntimeException( "Socket不能是null" ); } this.socket = socket; } @Override public void run() { try { // 获得 可以从 当前监听到的 客户端连接 中 读取数据的 字节输入流 InputStream in = socket.getInputStream(); Reader reader = new InputStreamReader( in ) ; BufferedReader br = new BufferedReader( reader ); while( true ) { String message = br.readLine(); String msg = message ; message = "[ " + name +" ] 说 : [ " + message + " ]"; System.out.println( message ); for( Socket s :SOCKETS.values() ){ OutputStream out = s.getOutputStream(); PrintStream ps = new PrintStream( out ); ps.println( message ) ; } if( "byebyebye".equalsIgnoreCase( msg ) ){ SOCKETS.remove( name ); break ; } } } catch (IOException e) { if( e instanceof SocketException ){ SocketException se = (SocketException)e ; String message = se.getMessage() ; if( "Connection reset".equalsIgnoreCase( message ) ) { System.out.println( "客户端断开" ); // 将已经废弃的 Socket 对象从 Map 集合中移除 SOCKETS.remove( name ); } else { se.printStackTrace(); } } else { e.printStackTrace(); } } } } }
package ecut.network; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.Reader; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.util.Scanner; public class ClientV4 { public static void main(String[] args) throws IOException { final Scanner scanner = new Scanner( System.in ); // 1、创建一个可以连接远程服务器的一个 Socket 对象 ( 充当客户端 ) Socket client = new Socket(); SocketAddress remote = new InetSocketAddress( "192.168.0.106", 6666 ); // 2、连接 "远程" 服务器上的指定程序 client.connect( remote ); // 3、创建一个独立的线程,专门用来读取服务器发送的数据 ClientThread t = new ClientThread( client ); t.start(); // 4、获得可以向服务器输出数据的 字节输出流 OutputStream out = client.getOutputStream(); PrintStream ps = new PrintStream( out ); while( true ){ System.out.println( "请输入你要向服务器发送的信息:" ); String s = scanner.nextLine(); ps.println( s ); if( "byebyebye".equalsIgnoreCase( s ) ){ break ; } } // end client.close(); scanner.close(); } public static class ClientThread extends Thread { private Socket socket ; public ClientThread(Socket socket) { super(); this.setDaemon( true );//应该设置为精灵线程 this.socket = socket; } @Override public void run() { try { // 获得 可以从 当前监听到的 客户端连接 中 读取数据的 字节输入流 InputStream in = socket.getInputStream(); Reader reader = new InputStreamReader( in ) ; BufferedReader br = new BufferedReader( reader ); while( true ) { String s = br.readLine(); System.out.println( s ); } } catch (IOException e) { if( e instanceof SocketException ){ SocketException se = (SocketException)e ; String message = se.getMessage() ; if( "Connection reset".equalsIgnoreCase( message ) ) { System.out.println( "服务器已关闭" ); } else { se.printStackTrace(); } } else { e.printStackTrace(); } } } } }
ServerSocket / Socket
Socket 插板
服务器:被动地、对外提供某种服务或多种服务
ServerV4 监听客户端连接、将监听到的客户端加入集合、启动服务线程
ClientV4 连接远程服务器、启动接受数据的线程、向服务器发送数据
运行结果如下:
服务器启动中... 服务器正在初始化... 服务器初始化完成,准备对外提供服务 服务器正在监听来自客户端的连接... 创建为[ 192.168.0.106:52916 ]提供服务的线程 启动为[ 192.168.0.106:52916 ]提供服务的线程 服务器正在监听来自客户端的连接... 创建为[ 192.168.0.106:52917 ]提供服务的线程 启动为[ 192.168.0.106:52917 ]提供服务的线程 服务器正在监听来自客户端的连接... [ 192.168.0.106:52916 ] 说 : [ hello ] [ 192.168.0.106:52917 ] 说 : [ hi ] [ 192.168.0.106:52916 ] 说 : [ nice to meet you ] [ 192.168.0.106:52917 ] 说 : [ nice to meet you too ] [ 192.168.0.106:52916 ] 说 : [ i have someting to do ,so see you later ] [ 192.168.0.106:52917 ] 说 : [ ok, ] [ 192.168.0.106:52916 ] 说 : [ byebyebye ] [ 192.168.0.106:52917 ] 说 : [ byebyebye ]
请输入你要向服务器发送的信息: hello 请输入你要向服务器发送的信息: [ 192.168.0.106:52916 ] 说 : [ hello ] [ 192.168.0.106:52917 ] 说 : [ hi ] nice to meet you 请输入你要向服务器发送的信息: [ 192.168.0.106:52916 ] 说 : [ nice to meet you ] [ 192.168.0.106:52917 ] 说 : [ nice to meet you too ] i have someting to do ,so see you later 请输入你要向服务器发送的信息: [ 192.168.0.106:52916 ] 说 : [ i have someting to do ,so see you later ] [ 192.168.0.106:52917 ] 说 : [ ok, ] byebyebye
请输入你要向服务器发送的信息: [ 192.168.0.106:52916 ] 说 : [ hello ] hi 请输入你要向服务器发送的信息: [ 192.168.0.106:52917 ] 说 : [ hi ] [ 192.168.0.106:52916 ] 说 : [ nice to meet you ] nice to meet you too 请输入你要向服务器发送的信息: [ 192.168.0.106:52917 ] 说 : [ nice to meet you too ] [ 192.168.0.106:52916 ] 说 : [ i have someting to do ,so see you later ] ok, 请输入你要向服务器发送的信息: [ 192.168.0.106:52917 ] 说 : [ ok, ] [ 192.168.0.106:52916 ] 说 : [ byebyebye ] byebyebye
基于ServerSocket / Socket 的聊天室图解:
UDP
1、UDP : 解决数据传输问题 ,非面向连接、不可靠的数据传输协议 ( 代价较小 ),突发性的对数据传输要求较低的。
2、数据报(Datagrama):
通过网络传输的数据的基本单元
- 包含一个报头(header)和数据本身。
- 其中报头描述了数据的目的地以及和其它数据之间的关系。
数据报工作方式的特点
- 同一报文的不同分组可以由不同的传输路径通过通信子网。
- 同一报文的不同分组到达目的结点时可能出现乱序、重复与丢失现象。
- 每一个分组在传输过程中都必须带有目的地址与源地址。
- 数据报方式报文传输延迟较大,适用于突发性通信,不适用于长报文、会话式通信。
3、IP数据报( IP Datagram )
TCP/IP 协议定义的在因特网上传输的包
- 这是一个与硬件无关的虚拟包, 由首部和数据两部分组成。
- 首部的前一部分是固定长度,共20字节,是所有IP数据报必须具有的。
- 在首部的固定部分的后面是一些可选字段,其长度是可变的。
- 首部中的源地址和目的地址都是IP协议地址。
4、用户数据报( User Datagram )
基于UDP 在因特网上传输的数据包
- UDP在IP数据报的头部仅仅加入了复用和数据校验。
- UDP首部字段由4个部分组成,其中两个是可选的。
來源端口和目的端口用来标记发送和接受的应用进程
» 因UDP不需要应答,故來源端口是可选的,若來源端口不用,那么置为零
在目的端口后面是长度固定的以字节为单位的长度域
» 用来指定UDP数据报包括数据部分的长度,长度最小值为8byte。
首部剩下的部分是用来对首部和数据部分一起做校驗和(Checksum)的
» 这部分是可选的,但在实际应用中一般都使用这一功能
用户数据报协议在网络中的地位
数据报:报头(header)+报文。
IP数据报:首部(做出了明确划分和规定)+数据。
用户数据报:首部+数据(首部+数据)。
5、TCP 与UDP 的对比
面向连接的TCP :可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。
面向非连接的UDP :不可靠,差错控制开销较小,传输大小限制在64K以下,不需要建立连接(虚拟数
据链路)。
6、Java 中的UDP 通信技术核心api
DatagramPacket ( 数据报包)
- 该类的构造都需要接受一个字节数组做参数: byte[] buff
- 该字节数组即为需要传送的数据
- 该类中有很多重载的构造,可以根据实际情况来调用
DatagramSocket
- 重载的构造比较多,根据实际情况来调用。
- 常用方法:
bind(SocketAddress addr) 将此DatagramSocket绑定到特定的地址和端口。
close() 关闭此DatagramSocket。
InetAddress getLocalAddress() 获取DatagramSocket 绑定的本地地址。
int getLocalPort() 返回此DatagramSocket 绑定的本地主机上的端口号。
void receive(DatagramPacket p) 从此DatagramSocket 接收数据报包。
void send(DatagramPacket p) 从此DatagramSocket 发送数据报包。
DatagramChannel : UDP 的NIO 支持
- 该类是抽象类,因此构造不能被用来创建对象。
- 常用方法:
open() : 静态方法,用于打开一个数据报通道。
receive(ByteBuffer dst) 通过此通道接收数据报。
send(ByteBuffer src, SocketAddress target) 通过此通道发送数据报。
三个重载的write() 用于将数据报写入此通道。
socket() 获取与此通道关联的数据报套接字。
- 继承的方法:
该类继承了java.nio.channels.spi.AbstractSelectableChannel 和java.nio.channels.SelectableChannel ,这两个类中的方法均可使用。
- DatagramChannel 也可以注册于Selector 实例。
基于UDP协议的收发测试案例:
package ecut.network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.net.SocketAddress; public class Receiver1 { public static void main(String[] args) throws IOException { // 指定本地的 IP 地址和 端口 SocketAddress local = new InetSocketAddress( "192.168.0.106" , 5555 ) ; // 创建一个可以基于UDP 协议进行 收、发 数据的 DatagramSocket 对象 DatagramSocket receiver = new DatagramSocket( local ); byte[] buffer = new byte[ 1024 ]; // 创建一个用来接受数据的数据报包 对象 DatagramPacket dp = new DatagramPacket( buffer , buffer.length ); // 接受数据 ( 接受时,依然是以数据报包形式接受 ) receiver.receive( dp ); byte[] data =dp.getData() ; // 从数据报包 中获取接受到的数据 ( 长度可能超出实际数据的长度 ) System.out.println( buffer == data ); // true 说明 buffer 和 data 是同一个数组对象 int n = dp.getLength() ; // 接受到的字节数组的实际长度 System.out.println( new String( data , 0 , n ) ); receiver.close(); } }
package ecut.network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.net.SocketAddress; public class Sender1 { public static void main(String[] args) throws IOException { // 指定本地的 IP 地址和 端口 SocketAddress local = new InetSocketAddress( "192.168.0.106" , 2222 ) ; // 创建一个可以基于UDP 协议进行 收、发 数据的 DatagramSocket 对象 DatagramSocket sender = new DatagramSocket( local ); // 指定远程IP地址和端口 ( 就是 数据报包 的目的地址 ) SocketAddress remote = new InetSocketAddress( "192.168.0.106" , 5555 ); // 确定要发送的数据 byte[] buffer = "今天天气不好?好?".getBytes() ; // 构造数据报包 ( 并指定发送的数据 ) DatagramPacket dp = new DatagramPacket( buffer, buffer.length ); // 为数据报包 指定目的地址 dp.setSocketAddress( remote ); // 发送数据报包 sender.send( dp ); sender.close(); } }
运行结果如下:
true 今天天气不好?好?
基于UDP协议的多次收发测试案例一:
package ecut.network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.net.SocketAddress; public class Receiver2 { public static void main(String[] args) throws IOException { // 指定本地的 IP 地址和 端口 SocketAddress local = new InetSocketAddress( "192.168.0.106" , 5555 ) ; // 创建一个可以基于UDP 协议进行 收、发 数据的 DatagramSocket 对象 DatagramSocket receiver = new DatagramSocket( local ); final byte[] buffer = new byte[ 1024 ]; // 创建一个用来接受数据的数据报包 对象 DatagramPacket dp = new DatagramPacket( buffer , buffer.length ); while( true ) { // 接受数据 ( 接受时,依然是以数据报包形式接受 ) receiver.receive( dp ); //byte[] data =dp.getData() ; // 从数据报包 中获取接受到的数据 ( 长度可能超出实际数据的长度 ) int n = dp.getLength() ; // 接受到的字节数组的实际长度 String s = new String( buffer , 0 , n ); System.out.println( s ); if( "byebyebye".equalsIgnoreCase( s ) ){ break ; } } receiver.close(); } }
package ecut.network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Scanner; public class Sender2 { public static void main(String[] args) throws IOException { Scanner scan = new Scanner( System.in ); // 指定本地的 IP 地址和 端口 SocketAddress local = new InetSocketAddress( "192.168.0.106" , 2222 ) ; // 创建一个可以基于UDP 协议进行 收、发 数据的 DatagramSocket 对象 DatagramSocket sender = new DatagramSocket( local ); // 指定远程IP地址和端口 ( 就是 数据报包 的目的地址 ) SocketAddress remote = new InetSocketAddress( "192.168.0.106" , 5555 ); final byte[] buffer = { }; DatagramPacket dp = new DatagramPacket( buffer, buffer.length ); // 为数据报包 指定目的地址 dp.setSocketAddress( remote ); String s ; System.out.println( "请输入你要发送的数据:" ); while( ( s = scan.nextLine() ) != null ){ byte[] data = s.getBytes() ; // 每次发送之前都将要发送的数据设置到 数据报包 中 dp.setData( data ); // 发送数据报包 sender.send( dp ); if( "byebyebye".equalsIgnoreCase( s ) ){ break ; } System.out.println( "请输入你要发送的数据:" ); } sender.close(); scan.close(); } }
运行结果如下:
world
hello
hi
hello
byebyebye
请输入你要发送的数据:
world
请输入你要发送的数据:
hello
请输入你要发送的数据:
byebyebye
请输入你要发送的数据:
hi
请输入你要发送的数据:
hello
请输入你要发送的数据:
byebyebye
基于UDP协议的多次收发测试案例二:
package ecut.network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.net.SocketAddress; public class Receiver3 { public static void main(String[] args) throws IOException { // 指定本地的 IP 地址和 端口 SocketAddress local = new InetSocketAddress( "192.168.0.106" , 5555 ) ; // 创建一个可以基于UDP 协议进行 收、发 数据的 DatagramSocket 对象 DatagramSocket receiver = new DatagramSocket( local ); final byte[] buffer = new byte[ 1024 ]; // 创建一个用来接受数据的数据报包 对象 DatagramPacket dp = new DatagramPacket( buffer , buffer.length ); while( true ) { // 接受数据 ( 接受时,依然是以数据报包形式接受 ) receiver.receive( dp ); InetSocketAddress remote = (InetSocketAddress)dp.getSocketAddress(); // 作为接收者,获得发送者对应的 IP 地址 和 端口 String sender = remote.getHostString() + ":" + remote.getPort() ; int n = dp.getLength() ; // 接受到的字节数组的实际长度 String s = new String( buffer , 0 , n ); System.out.println( sender + "说: " + s ); if( "byebyebye".equalsIgnoreCase( s ) ){ break ; } } receiver.close(); } }
运行结果如下:
192.168.0.106:2222说: hello 192.168.0.106:6666说: hi 192.168.0.106:6666说: byebye 192.168.0.106:2222说: byebyebye
hello
请输入你要发送的数据:
byebyebye
请输入你要发送的数据:
hi
请输入你要发送的数据:
byebye
请输入你要发送的数据:
byebyebye
MulticastSocket
1、多播数据报套接字类用于发送和接收 IP 多播包
MulticastSocket 是一种(UDP) DatagramSocket。
它具有加入Internet 上其他多播主机的“组”的附加功能。
多播组通过D 类IP 地址和标准UDP 端口号指定。
- D 类IP 地址在224.0.0.0 和239.255.255.255 的范围内(包括两者)。
- 地址224.0.0.0 被保留,不应使用。
构造方法:
- MulticastSocket()使用本机默认地址、随机端口来创建MulticastSocket 对象。
- MulticastSocket(int port)使用本机默认地址和指定端口号来创建MulticastSocket 对象。
- MulticastSocket( SocketAddress bindaddr )创建绑定到指SocketAddress 的MulticastSocket 对象该SocketAddress 中包含了IP 地址和端口号。
常用方法:
- void joinGroup(InetAddress mcastaddr)加入多播组。
- void joinGroup( SocketAddress sa, NetworkInterface ni )加入指定接口上的指定多播组。
- void leaveGroup( InetAddress ia )离开多播组。
- void leaveGroup( SocketAddress sa, NetworkInterface ni )离开指定本地接口上的多播组。
- int getTimeToLive()获取在套接字上发出的多播数据包的默认生存时间。
- void setTimeToLive(int ttl)设置所发出的多播数据包的默认生存时间(用以控制多播的范围)。
ttl = 0 , 表示指定数据报应停留在本地主机。
ttl = 1 , 表示指定数据报发送到本地局域网(这是默认值)。
ttl = 32 , 表示指定数据报只能发送到本站的的网络上。
ttl = 64 , 表示指定数据报应保留在本地区。
ttl = 128 , 表示指定数据报应保留在本大洲。
ttl = 255 , 表示指定数据报可以发送到Internet 的所有地方。 - boolean getLoopbackMode()获取多播数据报的本地回送的设置。
- void setLoopbackMode(boolean disable)启用/禁用多播数据报的本地回送。
- NetworkInterface getNetworkInterface()获取多播网络接口集合。
- void setNetworkInterface(NetworkInterface netIf)指定在此套接字上发送的输出多播数据报的网络接口。
- InetAddress getInterface()获取用于多播数据包的网络接口的地址。
- void setInterface(InetAddress inf)设置多播网络接口,供其行为将受网络接口值影响的方法使用。
MulticastSocket测试案例:
package ecut.network; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.util.Scanner; public class MulticastSocketTest { public static void main(String[] args) throws IOException { byte[] addr = { (byte)224, 0, 0 , 1 }; // 创建 多播组 对应的 IP 地址 ( 224.0.0.0 ~ 239.255.255.255 ) final InetAddress group = InetAddress.getByAddress( addr ); final int port = 445 ; // 创建可以实现多点广播的 MulticastSocket 对象 MulticastSocket node = new MulticastSocket( port ); // 让当前的 "节点" 加入指定的 多播组 node.joinGroup( group ); // 创建一个独立的接受数据的线程 ReceiveThread t = new ReceiveThread( node ); t.start(); // 启动线程 // 创建一个数据报包对象 ( 其中 "没有" 要发送的数据 ) DatagramPacket dp = new DatagramPacket( new byte[0] , 0 ); // 指定数据报包的目的地址 dp.setAddress( group ); // 指定数据报包的目的端口 dp.setPort( port ); Scanner scanner = new Scanner( System.in ); while( true ){ System.out.println( "请输入你要发送的信息: " ); String s = scanner.nextLine(); byte[] data = s.getBytes() ; // 将 字符串 根据当前默认平台编码编码为字节数组 ( String ---> byte[] ) dp.setData( data ); // 设置将要发送的数据 node.send( dp ); // 发送数据 if( "byebyebye".equalsIgnoreCase( s ) ){ break; } } scanner.close(); node.leaveGroup( group ); node.close(); } public static class ReceiveThread extends Thread { private MulticastSocket currentNode ; public ReceiveThread(MulticastSocket currentNode) { super(); this.currentNode = currentNode; this.setDaemon( true ); // 守护线程 } @Override public void run() { final byte[] buffer = new byte[ 1 << 16 ] ; DatagramPacket dp = new DatagramPacket( buffer , buffer.length ); while( true ) { try { System.out.println( "接受数据" ); currentNode.receive( dp ); String sender = dp.getAddress().getHostAddress() + ":" + dp.getPort() ; int len = dp.getLength(); String message = new String( buffer , 0 , len ) ; System.out.println( "[" + sender +"] 说 :[ " + message + " ]" ); } catch (IOException e) { System.err.println( e.getMessage() ); } } } } }
DatagramSocket仅仅同意数据报发送给指定的目标地址,而MulticastSocket能够将数据报以广播的方式发送到多个client。
若要使用多点广播,则须要让一个数据报标有一组目标主机地址,当数据报发出后,整个组的全部主机都能收到该数据报。IP多点广播(或多点发送)实现了将单一信息发送到多个接受者的广播,其思想是设置一组特殊网络地址作为多点广播地址,每个多点广播地址都被看做一个组,当client须要发送、接收广播信息时,增加到改组就可以。
MulticastSocket既能够将数据报发送到多点广播地址,也能够接收其它主机的广播信息。
待解决问题
MulticastSocket
转载请于明显处标明出处