Java-Day-28( 网络相关概念 + InetAddress 类 + Socket + TCP 网络通信编程 + netstat 指令 )
Java-Day-28
网络相关概念
网络通信
- 两台设备之间通过网络实现数据传输 ( 将数据通过网络从一台设备传输到另一台设备 )
- java.net 包下提供了一系列的类或接口,供程序员使用,完成网络通信
网络
- 两台或多台设备通过一定物理设备连接起来构成了网络
- 根据网络的覆盖范围不同,对网络进行分类:
- 局域网:覆盖范围最小,仅仅覆盖一个教室或一个机房
- 城域网:覆盖范围较大,可以覆盖一个城市
- 广域网:覆盖范围最大,可以覆盖全国,甚至全球,万维网是广域网的代表
ip 地址
- 用于唯一标识网络中的每台计算机 / 主机
- 查看 ip 地址:ipconfig
- ip 地址的表示形式:点分十进制
- 如 IPV4:4 个字节 ( 乘以 8 得 32 位 ) 表示
- 每一个十进制数的范围:0 ~255 ( 一个字节 )
- ip 地址的组成 = 网络地址 + 主机地址,如:192.168.18.69 ( 仅 69 指主机地址 )
- ilPv6 是互联网工程任务组设计的用于替代 IPv4 的下一代 IP 协议,其地址数量号称可以为全世界的每一粒沙子编上一个地址
- 4 暂只考虑了主机等,但未来更多的网络设备会接入到网络中 ( 万物互联 ),如物联网,所以 4 不够用,就需要 6 来逐渐替代扩增
- 由于 IPv4 最大的问题在于网络地址资源有限,严重制约了互联网的应用和发展。Ipv6 的使用,不仅能解决网络地址资源数量的问题,而且也解决了多种接入设备连入互联网的障碍
- IPv6 是用 128 位表示一个地址 ( 除以 8 得 16 个字节 )
- 例:2408 : 8207 : 1851 : 8670 : d52d : 7a14 : 362c : 4e8d ( IPv6 地址显示为 8 段 )
- 16 ( 字节 ) 除以 8 ( 段 ) 得 2,即一段为两个字节,即 16 个 bits ( 位 )
ipv4 地址分类
- A 类:0 开头一个字节表示一个网络号,其余都是主机号 ( 可以引更多的主机:2 ^ 24 - 1 )
- 0.0.0.0 ~ 127.255.255.255
- B 类:1 0 开头两个字节表示一个网络号
- 128.0.0.0 ~ 191.255.255.255
- C 类:1 1 0 开头三个字节表示一个网络号
- 192.0.0.0 ~ 233.255.255.255
- D 类:1 1 1 0 开头
- 224.0.0.0 ~ 239.255.255.255
- E 类:1 1 1 1 0 开头
- 240.0.0.0 ~ 247.255.255.255
- 特殊的 127.0.0.1 表示本机地址
域名
-
将 ip 地址映射成域名 ( HTTP 协议 )
-
例:www.baidu.com
-
为了方便记忆,解决记 ip 的困难
端口号
- 用于标识计算机上某个特定的网络程序
- 用 IP + 端口:找到特定服务 ( IP 指一个酒店,端口就是酒店不同规格房间,所对应的不同的房间号 )
- 表示形式:以整数形式,端口范围 0 ~ 65535
- 两个字节表示端口
- 2 ^ 16 - 1 = 65536 - 1 = 65535
- 两个字节表示端口
- 0 ~ 1024 已经被占用 ( 开发时就不要使用了 )
- 比如:ssh 22, ftp 21, smtp 25, http 80
- 常见的网络程序端口号
- tomcat:8080
- mysql:3306
- oracle:1521
- sqlserver:1433
网络通信协议
-
协议 ( tcp / ip )
- 如 QQ 信息聊天时,A 给 B 发信息,就是发送数据,而在网络编程中数据的组织形式,就是协议
- 然后 B 回复数据给 A,就也是按照规定好的协议方式
- QQ 程序负责将协议的内容取出呈现出来
-
TCP / IP ( Transmission Control Protocol / Internet Protocol ),中文译名为传输控制协议 / 因特网互联协议,又名网络通讯协议,这个协议是 Internet 最基本的协议、Internet 国际互联网络的基础,简单来说,就是由网络层的 IP 协议和传输层的 TCP 协议组成的。
-
简单一叙:
- 用户数据 —( 应用程序 )—> 加上了 App 首部 —( TCP )—> 加上了 TCP 首部 —( IP )—> 加上了 IP 首部 —( 以太网驱动程序 )—> 加上以太网首部与尾部 ——> 打包完成进入以太网发送出
- 接收时就是倒过来一步步拆包直到仅用户数据
-
TCP / IP 协议包括两种模型:
- OSI 模型 ( 理论模型 ):应用层 —— 表示层 —— 会话层 —— 传输层 —— 网络层 —— 数据链路层 —— 物理层
- TCP / IP 模型 ( 实际所用 ):应用层 —— 传输层 —— 网络层 —— 物理 + 数据链路层
- 都与上面简单一叙有对应:应用程序 ( HTTP 等协议 ) —— TCP 层 ( TCP 协议用最多,所以有的就称 TCP 层 ) —— IP 层 ( IP 协议用最多,就叫 IP 层了 ) —— Link 协议
TCP 和 UDP
- TCP 协议:传输控制协议 —— 慢但可靠
- 使用 TCP 协议前,须先建立 TCP 连接,形成传输数据通道
- 传输前,采用 " 三次握手 " 方式,是可靠的
- A 发送给连接请求给 B
- B 回复确认信息给 A 并向 A 也发生连接请求
- A 收到后,回复 B 确认信息
- ...... 发送正式信息 ......
- TCP 协议进行通信的两个应用进程:客户端、服务端
- 在连接种可进行大数据量的传输
- 传输完毕,需释放已建立的连接 ( 否则无法接收或发送信息给其他 ),效率低
- UDP 协议:—— 快但不可靠
- 将数据、源、目的封装成数据包,不需要建立连接
- 每个数据报的大小限制在 64 K 内
- 因无需链接,故是不可靠的
- A 直接通知 B 后就紧接着走了
- 发送数据结束时无需释放资源 ( 因为不是面向连接的 ),速度快
InetAddress 类
-
相关方法
- 获取本机 InetAddress 对象:getLocalHost
- 根据指定主机名 / 域名获取 ip 地址对象:getByName
- 获取 InetAddress 对象的主机名:getHostName
- 获取 InetAddress 对象的地址:getHostAddress
public class Test_java { public static void main(String[] args) throws UnknownHostException { // 获取本机的 InetAddress 对象 InetAddress localHost = InetAddress.getLocalHost(); System.out.println("计算机名/IP —— " + localHost); // 计算机名/IP —— DESKTOP-TO5JTRS/10.111.4.162 // 根据指定主机名获取 InetAddress 对象 InetAddress host1 = InetAddress.getByName("DESKTOP-TO5JTRS"); System.out.println("host1 = " + host1); // DESKTOP-TO5JTRS/10.111.4.162 // 根据域名返回 InetAddress 对象 InetAddress host2 = InetAddress.getByName("www.baidu.com"); System.out.println("host2 (域名/IP) = " + host2); // host2 = www.baidu.com/110.242.68.3 // 通过 InetAddress 对象,获取对应的地址 String hostAddress = host2.getHostAddress(); // (百度的IP) 会变化 System.out.println("host2对应的IP = " + hostAddress); // 通过 InetAddress 对象,获取对应的主机名/域名 String hostName = host2.getHostName(); System.out.println("host2对应的主机名/域名 = " + hostName); // www.baidu.com (这个不会变的) } }
Socket
-
套接字 ( Socket ) 开发网络应用程序被广泛采用,以至于成为事实上的标准
-
通信的两端都要有 Socket,是两台机器间通信的端点
-
网络通信其实就是 Socket 间的通信
-
Socket 允许程序把网络连接当成一个流,数据在两个 Socket 间通过 IO 传输
-
一般主动发起通信的应用程序属客户端,等待通信请求的为服务端
-
两种编程方式:TCP 编程和 UDP 编程
-
简单理解:
- 主机 A ( 客户端 — 发起连接 ) 的 socket ——— 数据通道 ———> 主机 B ( 服务端 — 接收请求连接 ) 的 socket
- A 调用 socket.getOutputStream 输出流写到数据通道,由 B 调用 socket.getInputStream 输入流读取通道的数据,将数据读到了程序中去
- 读写操作反之亦可 ( in 为读进,out 为写出 )
- 主机 A ( 客户端 — 发起连接 ) 的 socket ——— 数据通道 ———> 主机 B ( 服务端 — 接收请求连接 ) 的 socket
TCP 网络通信编程
-
基于客户端 — 服务端的网络通信
-
底层使用的是 TCP / IP 协议
-
应用场景举例:客户端发送数据,服务端接收并显示
-
基于 Socket 的 TCP 编程
- 即 Server 服务端与 Client 客户端建立连接后,便如上 " 简单理解 " 部分所述,完成相互的信息传输操作后,都勿忘了要 Socket.close() 关闭
应用案例一 ( 使用字节流 + 单向信息 ) :
-
编写一个服务器端和一个客户端
-
服务器端在 9999 端口监听
- 当没有客户端连接 9999 端口时,程序会阻塞,等待连接
- 连接后,生成一个 Socket 输入流读取数据
-
客户端连接到服务器端,发送 " hello server, I'm Client ",然后退出
- 连接服务端 ( ip,端口 ),连接上后,生成一个 Socket,通过输出流写入数据到数据通道
-
服务器端接收到客户端发送的信息,输出,并退出
-
代码编写 — 客户端:
/** * 客户端 */ public class SocketTCP01Client { public static void main(String[] args) throws IOException { // 通过 ip/端口 连接服务端 (这里仿照有一个本地的9999接口) Socket socket = new Socket(InetAddress.getLocalHost(), 9999); // 因为演示是在一个电脑上,所以就获取本机,连接本机的9999端口 // 如果连接成功就返回 Socket 对象 // 如果是远程服务器,就写 "ip/端口" System.out.println("客户端 socket 返回 = " + socket.getClass()); // 连接后,生成的Socket通过get方法获取与socket对象关联的输出流对象 OutputStream outputStream = socket.getOutputStream(); //// OutputStream 是抽象类,这里的运行类型实际上是SocketOutputStream,不过向上用OutputStream接收了 // System.out.println("socket.getOutputStream的运行类型 :" + outputStream.getClass()); // 通过输出流,写入数据到数据通道 outputStream.write("hello server, I'm Client".getBytes()); // 别忘了关闭流对象和socket outputStream.close(); socket.close(); System.out.println("信息已发送,客户端已退出......"); } }
-
代码编写 — 服务端:
/** * 服务端 */ public class SocketTCP01Server { public static void main(String[] args) throws IOException { // 服务器端为了应对多个客户端的连接,所以用一个serverSocket来accept返回出多个socket ServerSocket serverSocket = new ServerSocket(9999); // 细节:要求在本机没有其他服务在监听9999端口 System.out.println("服务端在9999端口监听,等待连接中......"); // 当有客户端连接时,就返回一个对象:socket Socket socket = serverSocket.accept(); // 没有连接的话,运行就阻塞在这,不往下走了 System.out.println("服务器端 socket = " + socket.getClass()); // 通过 socket.getInputStream 读取客户端写入到数据通道的数据 InputStream inputStream = socket.getInputStream(); // IO 读取 (字节流返回的都是数字,字符流返回的空判断才是用null) System.out.println("读取Client通过数据通道写入的数据并呈现~~"); byte[] buf = new byte[1024]; int readLen = 0; while ((readLen = inputStream.read(buf)) != -1) { System.out.println(new String(buf, 0, readLen)); // 根据读取到的实际长度,显示内容 } // 勿忘关闭流和socket inputStream.close(); socket.close(); serverSocket.close(); // 以防资源的浪费、占用 } }
应用案例二 ( 使用字节流 + 双向信息 ) :
-
编写一个服务器端和一个客户端
-
服务器端在 9999 端口监听
-
客户端连接到服务器端,发送 " hello server, I'm Client ",并接收服务器端回发的 " hello client, baga ",然后再退出
-
服务器端接收到客户端发送的信息,输出,并发送 " hello client, baga " 再退出
-
代码编写 — 客户端:
/** * 客户端 */ public class SocketTCP02Client { public static void main(String[] args) throws IOException { // 通过 ip/端口 连接服务端 (这里仿照有一个本地的9999接口) Socket socket = new Socket(InetAddress.getLocalHost(), 9999); // 因为演示是在一个电脑上,所以就获取本机,连接本机的9999端口 // 如果连接成功就返回 Socket 对象 // 如果是远程服务器,就写 "ip/端口" System.out.println("客户端 socket 返回 = " + socket.getClass()); // 连接后,生成的Socket通过get方法获取与socket对象关联的输出流对象 OutputStream outputStream = socket.getOutputStream(); //// OutputStream 是抽象类,这里的运行类型实际上是SocketOutputStream,不过向上用OutputStream接收了 // System.out.println("socket.getOutputStream的运行类型 :" + outputStream.getClass()); // 通过输出流,写入数据到数据通道 outputStream.write("hello server, I'm Client".getBytes()); // 设置结束标记 socket.shutdownOutput(); // 获取相关联的输入流读取数据(字节)并显示 InputStream inputStream = socket.getInputStream(); byte[] buf = new byte[1024]; int readLen = 0; while ((readLen = inputStream.read(buf)) != -1) { System.out.println(new String(buf, 0, readLen)); } // 别忘了关闭流对象和socket outputStream.close(); inputStream.close(); socket.close(); System.out.println("信息已发送,客户端已退出......"); } }
-
代码编写 — 服务端:
/** * 服务端 */ public class SocketTCP02Server { public static void main(String[] args) throws IOException { // 服务器端为了应对多个客户端的连接,所以用一个serverSocket来accept返回出多个socket ServerSocket serverSocket = new ServerSocket(9999); // 细节:要求在本机没有其他服务在监听9999端口 System.out.println("服务端在9999端口监听,等待连接中......"); // 当有客户端连接时,就返回一个对象:socket Socket socket = serverSocket.accept(); // 没有连接的话,运行就阻塞在这,不往下走了 System.out.println("服务器端 socket = " + socket.getClass()); // 通过 socket.getInputStream 读取客户端写入到数据通道的数据 InputStream inputStream = socket.getInputStream(); // IO 读取 (字节流返回的都是数字,字符流返回的空判断才是用null) System.out.println("读取Client通过数据通道写入的数据并呈现~~"); byte[] buf = new byte[1024]; int readLen = 0; while ((readLen = inputStream.read(buf)) != -1) { System.out.println(new String(buf, 0, readLen)); // 根据读取到的实际长度,显示内容 } OutputStream outputStream = socket.getOutputStream(); outputStream.write("server: hello client, baga".getBytes()); // 设置结束标记 socket.shutdownOutput(); // 勿忘关闭流和socket inputStream.close(); outputStream.close(); socket.close(); serverSocket.close(); // 以防资源的浪费、占用 } }
-
注意:因为有来回的对话,不像案例一发完就退,所以要在 socket 设置结束标记 ( 即:socket.shutdownOutput() ) ,表示已完成了信息的发送
应用案例三 ( 使用字符流,使在发送文本的时候更方便些 ) :
-
既然要用字符流,就 OutputStream —> Writer,InputStream —> Reader
-
编写一个服务器端和一个客户端
-
服务器端在 9999 端口监听
-
客户端连接到服务器端,发送 " hello server, I'm Client ",并接收服务器端回发的 " hello client, baga ",然后再退出
-
服务器端接收到客户端发送的信息,输出,并发送 " hello client, baga " 再退出
-
代码编写 — 客户端:
public class SocketTCP03Client { public static void main(String[] args) throws IOException { Socket socket = new Socket(InetAddress.getLocalHost(), 9999); // 因为演示是在一个电脑上,所以就获取本机,连接本机的9999端口 System.out.println("客户端 socket 返回 = " + socket.getClass()); OutputStream outputStream = socket.getOutputStream(); // outputStream.write("hello server, I'm Client".getBytes()); ———> 字节流所用 // 使用字符流: BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream)); // OutputStreamWriter 转换流 bufferedWriter.write("hello server, I'm Client (字符流)"); bufferedWriter.newLine(); // 插入一个换行符,表示写入的内容结束(注意:要求对方用readLine()来读!!!!) bufferedWriter.flush(); // 如果使用的字符流,需要手动刷新,否则数据不会写入数据通道 InputStream inputStream = socket.getInputStream(); // byte[] buf = new byte[1024]; // int readLen = 0; // while ((readLen = inputStream.read(buf)) != -1) { // System.out.println(new String(buf, 0, readLen)); // } // 字符流方式: BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); // InputStreamReader 转换为字符流 String s = bufferedReader.readLine(); System.out.println(s); // 输出 outputStream.close(); inputStream.close(); socket.close(); System.out.println("信息已发送,客户端已退出......"); } }
-
代码编写 — 服务端:
public class SocketTCP03Server { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9999); System.out.println("服务端在9999端口监听,等待连接中......"); Socket socket = serverSocket.accept(); System.out.println("服务器端 socket = " + socket.getClass()); InputStream inputStream = socket.getInputStream(); System.out.println("读取Client通过数据通道写入的数据并呈现~~"); // byte[] buf = new byte[1024]; // int readLen = 0; // while ((readLen = inputStream.read(buf)) != -1) { // System.out.println(new String(buf, 0, readLen)); // } // 字符流方式: BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String s = bufferedReader.readLine(); System.out.println(s); OutputStream outputStream = socket.getOutputStream(); // outputStream.write("server: hello client, baga".getBytes()); // socket.shutdownOutput(); // 字符流方式: BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream)); bufferedWriter.write("hello client, baga (字符流)"); bufferedWriter.newLine(); // 写入一个换行符表示写入/回复内容的结束 bufferedWriter.flush(); // 需要手动刷新flush // inputStream.close(); // 关外层流 bufferedReader.close(); // outputStream.close(); bufferedWriter.close(); socket.close(); serverSocket.close(); } }
应用案例四 ( 图片 + 信息 )
-
编写一个服务器端和一个客户端
-
服务器端在 8888 端口监听
-
客户端连接到服务器端,发送一张图片 e:\\student.jpg
-
服务器端接收到客户端发送的图片,保存到 src 下,发送 " 收到图片 " 再退出
-
客户端接收到服务端发送的 " 收到图片 " 再退出
-
要求该程序使用 StreamUtils.java
-
一个辅助工具类
/** * 此类用于演示关于流的读写方法 */ public class StreamUtils { /** * 功能:将输入流转换成byte[],即可以把文件的内容读入到byte[] */ public static byte[] streamToByteArray(InputStream is) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); //创建输出流对象 byte[] b = new byte[1024]; // 字节数组 int len; while((len = is.read(b)) != -1) { // 循环读取 bos.write(b, 0, len); // 把读取到的数据写入bos } byte[] array = bos.toByteArray(); // 然后将bos转成字节数组(就是将图片转成程序内文件字节数组的过程) bos.close(); return array; } public static String streamToString(InputStream is) throws Exception { /** * 功能:输入流数据直接转成字符串 */ BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder builder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { builder.append(line + "\r\n"); } return builder.toString(); } }
-
-
使用 BufferedInputStream 和 BufferedOutputStream
-
思路:
- 将客户端的图片通过网络上传到服务器,服务器回复消息
- 客户端处,磁盘上的图片输入读到程序中,先放进一个字节数组 ( 图片是二进制 ),数据通过 socket 入输出流里 ——> 服务端输入流接收、读文件数据 ( 字节 ),然后输出到服务器的某目录下
- 服务端将信息 socket 输出,在客户端 socket 输入流,获取信息在程序里
-
代码编写 — 服务端:
public class SocketTCP04Server { public static void main(String[] args) throws Exception { // 1. 服务端在本机监听 8888 端口 ServerSocket serverSocket = new ServerSocket(8888); // 2. 等待连接 System.out.println("服务端8888端口监听等待......"); Socket socket = serverSocket.accept(); // 3. 读取客户端发送的数据 // 通过socket得到一个输入流 BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); byte[] bytes = StreamUtils.streamToByteArray(bis); // 4. 将得到的bytes数组(输出流)写入到指定的路径,就得到一个文件了 String desFilePath = "src\\main\\webapp\\img\\student.jpg"; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(desFilePath)); bos.write(bytes); bos.close(); // BufferedOutputStream字节流对图片,BufferedWriter字符流对文本 // 5. 向客户端回复 "收到图片" // 通过socket获取到输出流(字符) BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bufferedWriter.write("收到图片"); bufferedWriter.flush(); // 把内容刷新到数据通道 socket.shutdownOutput(); // 设置写入结束标记(否则读的时候不知道什么时候结束),此处用shut而不是换行符newLine // 关闭其他资源 bis.close(); socket.close(); serverSocket.close(); bufferedWriter.close(); } }
-
代码编写 — 客户端:
public class SocketTCP04Client { public static void main(String[] args) throws Exception { // 1. 客户端连接服务端,得Socket对象 Socket socket = new Socket(InetAddress.getLocalHost(), 8888); // 2. 创建读取磁盘文件的输入流 String filePath = "e:\\student.jpg"; BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath)); byte[] bytes = StreamUtils.streamToByteArray(bis); // 这里的bytes就是存的文件内容了 // 3. 通过socket获取到输出流,将bytes数据发送给服务端 BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream()); bos.write(bytes); // 将文件对应的字节数组写入到数据通道 bis.close(); socket.shutdownOutput(); // 设置写入数据的结束标记 // 采用了工具类方法,所以可直接用InputStream读取成字符串,否则还要readLen字节数组循环读取(new String...) // 接收从服务端回复的消息 InputStream inputStream = socket.getInputStream(); // 使用工具类的方法,直接将inputStream读取到的内容转成字符串 String s = StreamUtils.streamToString(inputStream); System.out.println(s); // 关闭相关流 bos.close(); socket.close(); inputStream.close(); } }
netstat 指令 ( TCP )
-
netstat -an:可以查看当前主机网络情况,包括端口监听情况和网络连接情况
-
netstat -an|more:可以分页显示 ( 回车显示下一条,空格显示下一页 )
- 可以输入 ctrl + c 直接退出
-
要求在 dos 控制台下执行
-
说明:
- Listening 表示某个端口在监听
- 如果有一个外部程序 ( 客户端 ) 连接到该端口,就会显示一条连接信息
-
netstat -anb:要找到命令提示符 ( 开始菜单的 Windows 系统里 ) 以管理员身份运行才行
-
同理后面加上:|more 就可以分页显示
-
运行先前案例代码的 Server 部分后就会发现多了一条:
-
-
TCP 网络通讯不为人知的秘密
-
当客户端连接到服务端后,实际上客户端也是通过一个端口和服务器端进行通讯的,这个端口是 TCP / IP 来分配的 ( 是不确定的,随机的 )
-
验证:在运行 Client 后紧接着 netstat -an | more,就会发现有一个和 8888 对应的随机端口 ( 例如:60258 )
-
如:TCP 192.168.12.1:8888 192.168.12.1:60258
TCP 192.168.12.1:60258 192.168.12.1:8888
- 因为服务端和客户端在编码时都放在了同一台主机:本机上,所以有两个来回
- 实际上服务端只能看到第一条,客户端只能看到第二条
-