二、Java网络编程中的常用类
Java为了跨平台,在网络应用通信时是不允许直接调用操作系统接口的,而是由java.net包来提供网络功能。而是通过java.net包开发。
2.1 InetAddress类的使用
作用:封装计算机的IP地址和DNS(没有端口信息)
注:DNS是Domain Name System,域名系统。
特点:
这个类没有构造方法。如果要得到对象,只能通过静态方法:getLocalHost()、getByName()、 getAllByName()、 getAddress()、getHostName()
2.1.1 获取本机信息
package cn.it.bz.Socket; import java.net.InetAddress; import java.net.UnknownHostException; public class InetTest { public static void main(String[] args) throws UnknownHostException { //实例化本机InetAddress对象 InetAddress localHost = InetAddress.getLocalHost(); //获取当前计算机的IP System.out.println(localHost.getHostAddress()); //获取计算机名 System.out.println(localHost.getHostName()); } }
2.1.2 根据域名获取计算机的信息
根据域名获取计算机信息时需要使用getByName(“域名”)方法创建InetAddress对象。
package cn.it.bz.Socket; import java.net.InetAddress; import java.net.UnknownHostException; public class InetTest2 { public static void main(String[] args) throws UnknownHostException { //根据域名获取InetAddress对象 InetAddress inetAddress = InetAddress.getByName("www.baidu.com"); //获取IP System.out.println(inetAddress.getHostAddress()); //获取计算机(服务器)名字 System.out.println(inetAddress.getHostName()); } }
2.1.3根据IP地址获取计算机的信息
package cn.it.bz.Socket; import java.net.InetAddress; import java.net.UnknownHostException; public class InetTest3 { public static void main(String[] args) throws UnknownHostException { //根据计算机IP获取InetAddress对象 InetAddress inetAddress = InetAddress.getByName("39.156.66.18"); System.out.println(inetAddress.getHostName()); } }
2.2 InetSocketAddress类的使用
作用:包含IP和端口信息,常用于Socket通信。此类实现 IP 套接字地址(IP 地址 + 端口号),不依赖任何协议。
InetSocketAddress相比较InetAddress多了一个端口号,端口的作用:一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现。
那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。
package cn.it.bz.Socket; import java.net.InetSocketAddress; public class InetSocketTest { public static void main(String[] args) { InetSocketAddress inetSocketAddress = new InetSocketAddress("www.baidu.com",80); //获取IP地址 getAddress()先返回一个InetAddress对象才能获取到IP System.out.println(inetSocketAddress.getAddress().getHostAddress()); //获取计算机域名 System.out.println(inetSocketAddress.getHostName()); } }
2.3 URL的使用
IP地址标识了Internet上唯一的计算机,而URL则标识了这些计算机上的资源。 URL 代表一个统一资源定位符,它是指向互联网“资源”的指针。资源可以是简单的文件或目录,也可以是对更为复杂的对象的引用,例如对数据库或搜索引擎的查询。
为了方便程序员编程,JDK中提供了URL类,该类的全名是java.net.URL,有了这样一个类,就可以使用它的各种方法来对URL对象进行分割、合并等处理。
package cn.it.bz.Socket; import java.net.MalformedURLException; import java.net.URL; public class UrlTest { public static void main(String[] args) throws MalformedURLException { //创建URL对象 URL url = new URL("https://www.itbaizhan.com/search.html?kw=java"); System.out.println("获取与此URL相关联协议的默认端口:"+url.getDefaultPort()); System.out.println("访问资源:"+url.getFile()); System.out.println("主机名"+url.getHost()); System.out.println("访问资源路径:"+url.getPath()); System.out.println("协议:"+url.getProtocol()); System.out.println("参数部分:"+url.getQuery()); } }
https协议默认端口是443,http协议默认是80。
2.3.1 通过URL实现最简单的网络爬虫
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; public class UrlTest2 { public static void main(String[] args) throws MalformedURLException { URL url = new URL("https://www.itbaizhan.com/"); //url.openStream()返回的是字节流 try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(url.openStream()))) { StringBuilder stringBuilder = new StringBuilder(); String temp = ""; while ((temp = bufferedReader.readLine()) != null){ stringBuilder.append(temp); } System.out.println(stringBuilder); }catch (IOException e){ e.printStackTrace(); } } }
三、TCP通信的实现
3.1 TCP通信实现原理
TCP协议是面向的连接的,在通信时客户端与服务器端必须建立连接。在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client)程序,简称客户端,而在第一次通讯中等待连接的程序被称作服务器端(Server)程序,简称服务器。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。
请求-响应”模式:
Socket类:发送TCP消息(实际上就是客户端对象)。
ServerSocket类:创建服务器(实际上就是服务器对象)。
套接字Socket是一种进程间的数据交换机制。这些进程既可以在同一机器上,也可以在通过网络连接的不同机器上。换句话说,套接字起到通信端点的作用。单个套接字是一个端点,而一对套接字则构成一个双向通信信道,使非关联进程可以在本地或通过网络进行数据交换。一旦建立套接字连接,数据即可在相同或不同的系统中双向或单向发送,直到其中一个端点关闭连接。套接字与主机地址和端口地址相关联。主机地址就是客户端或服务器程序所在的主机的IP地址。端口地址是指客户端或服务器程序使用的主机的通信端口。
在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样,客户端和服务器通过套接字所建立的连接使用输入输出流进行通信。
TCP/IP套接字是最可靠的双向流协议,使用TCP/IP可以发送任意数量的数据。
实际上,套接字只是计算机上已编号的端口。如果发送方和接收方计算机确定好端口,他们就可以通信了。
客户端与服务器端的通信关系图:
TCP/IP通信连接的简单过程:
位于A计算机上的TCP/IP软件向B计算机发送包含端口号的消息,B计算机的TCP/IP软件接收该消息,并进行检查,查看是否有它知道的程序正在该端口上接收消息。如果有,他就将该消息交给这个程序。要使程序有效地运行,就必须有一个客户端和一个服务器。
通过Socket的编程顺序:
1.创建服务器ServerSocket,在创建时,定义ServerSocket的监听端口(在这个端口接收客户端发来的消息)
2.ServerSocket调用accept()方法,使之处于阻塞状态。accept方法监听服务器和客户端之间是否连接,一旦连接就基于客户端返回一个对应Socket对象,客户端和服务器之间的连接就是两个Socket对象之间的连接。
3.创建客户端Socket,并设置服务器的IP及端口。
4.客户端发出连接请求,建立连接。先通过三次握手建立客户端和服务器之间TCP协议的连接,然后通过Socket建立客户端和服务器之间的连接。客户端和服务器之间的连接是建立在协议连接基础之上的。(还可以这么理解。协议之间的连接好比打开了两个小区之间交流的通道,通过Socket建立的客户端和服务器之间的连接好比是建立了两个小区中住户之间的连接)
5.分别取得服务器和客户端Socket的InputStream和OutputStream。
6.利用Socket和ServerSocket进行数据传输。
7.关闭流及Socket。
3.1.1 创建服务器
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; //服务端 public class BasicSocketServer { public static void main(String[] args) { System.out.println("服务器启动,等待监听……"); //创建ServerSocket对象 try(ServerSocket serverSocket = new ServerSocket(8888); //监听8888端口,此时当前线程处于堵塞状态,当监听到客户端连接到该端口时解除阻塞状态。 //连接成功返回的就是客户端的Socket对象 Socket socket = serverSocket.accept(); //获取Socket中的输入流,以获取客户端发送到服务端的数据 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); ) { //读取客户端发送的数据 bufferedReader.readLine(); }catch (Exception e){ e.printStackTrace(); System.out.println("服务器启动失败……"); } } }
public
class ServerSocket implements java.io.Closeable
ServerSocket 实现了 Closeable接口会自动关闭流对象,前提是使用try-with-resource语法。
3.1.2 创建客户端
package cn.it.bz.Socket; import java.io.PrintWriter; import java.net.Socket; //客户端 public class BasicSocketClient { public static void main(String[] args) { //创建客户端对象 try(Socket socket = new Socket("127.0.0.1",8888); //创建向服务端发送数据的输出流对象 PrintWriter printWriter = new PrintWriter(socket.getOutputStream()); ) { printWriter.println("服务端!你好!"); printWriter.flush(); }catch (Exception e){ e.printStackTrace(); } } }
Socket 也实现了自动关闭的接口,前提是使用try-with-resource语法,但是不要忘了刷新。
3.1.3 ”丑话“说在前头
在socket中不管是客户端还是服务端在向输出流写入数据时,一般要调用printWriter.println();方法,而不是printWriter.write();方法。原因是:
在使用 Socket 进行网络通信时,如果需要向外写数据,可以通过 OutputStream 对象来实现。但是,即使调用了 write() 方法将数据写入到输出流中,并不能保证对方能够正常接收到该数据。这主要是因为在 TCP/IP 协议栈中,发送端和接收端之间的数据传输是通过网络缓冲区实现的。当发送端调用 write() 方法将数据写入到输出流中后,并不会立即将数据发送给接收端,而是先将数据存放到输出缓冲区中等待发送。当调用 write() 方法写入数据时,并不会同时确保该数据已经被成功接收,因此在网络中可能会发生丢包或拥塞等情况,从而导致发送数据失败或接收数据错误。
3.2 TCP单项通信
单向通信是指通信双方中,一方固定为发送端,一方则固定为接收端。
3.2.1创建服务端
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; //服务端 public class OneWaySocketServer { public static void main(String[] args) { System.out.println("服务器启动,开始监听……"); try(//实例化服务器对象 ServerSocket serverSocket = new ServerSocket(8888); //取得Socket Socket socket = serverSocket.accept(); //获取字符缓冲输入流 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())) ) { System.out.println("连接成功^_^"); //一直接受客户端的消息 while (true){ String s = bufferedReader.readLine(); if ("关闭".equals(s)){ break; } System.out.println("客户端传递来的数据:"+s); } }catch (Exception e){ e.printStackTrace(); System.out.println("服务器启动失败-_-"); } } }
3.2.2 创建客户端
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; //客户端 public class OneWaySocketClient { public static void main(String[] args) { //获取与服务端对应的Socket对象 try(Socket socket = new Socket("127.0.0.1",8888); //通过与服务端对应的Socket对象获取输出流对象 PrintWriter pw = new PrintWriter(socket.getOutputStream()); //通过与服务端对应的Socket对象获取输入流对象 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { //创建键盘输入对象 System.out.println("输入数据,输入exit退出程序"); Scanner scanner = new Scanner(System.in); while(true){ //通过键盘输入获取需要向服务端发送的消息 String str = scanner.nextLine(); //将消息发送到服务端 pw.println(str); pw.flush(); if("exit".equals(str)){ break; } } }catch(Exception e){ e.printStackTrace(); } } }
3.3 TCP双向通信
3.3.1 创建服务端
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; public class TwoWaySocketServer { public static void main(String[] args) { System.out.println("服务器启动,监听8888端口……"); try(ServerSocket serverSocket = new ServerSocket(8888); Socket socket = serverSocket.accept(); //字符输入流 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); //字符输出流 PrintWriter printWriter = new PrintWriter(socket.getOutputStream()) ) { //键盘输入 Scanner scanner = new Scanner(System.in); while (true){ //先读取客户端的消息 String s = bufferedReader.readLine(); System.out.println("客户端信息:"+s); //向客户端发送消息 String s1 = scanner.nextLine(); printWriter.write(s1); //刷新 printWriter.flush(); } }catch (Exception e){ e.printStackTrace(); } } }
3.3.2 创建客户端
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; //客户端 public class TwoWaySocketClient { public static void main(String[] args) { try(//创建客户端对象 Socket socket = new Socket("127.0.0.1",8888); //字符输出流 PrintWriter printWriter = new PrintWriter(socket.getOutputStream()); //字符输入流 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); Scanner scanner = new Scanner(System.in); ) { //键盘输入 System.out.println("(客户端)输入数据:"); while (true){ //发送数据 String s = scanner.nextLine(); printWriter.println(s); printWriter.write(s); printWriter.flush(); //接受数据 System.out.println("服务端消息:"+ bufferedReader.readLine()); } }catch (Exception e){ e.printStackTrace(); } } }
程序小bug:只能先从客户端开始说,然后服务端才能说。只能一个人一句话。问题解决见3.4
3.4 点对点聊天应用
3.4.1 创建服务端
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; //发送消息的线程 class Send extends Thread{ private Socket socket; //与客户端对应的socket public Send(Socket socket){ this.socket = socket; } //发送消息 public void sendMsg(){ try( //创建键盘输入对象 Scanner scanner = new Scanner(System.in); //创建向客户端发送消息的输出流对象 PrintWriter printWriter = new PrintWriter(this.socket.getOutputStream()) ) { while (true){ String msg = scanner.nextLine(); printWriter.println(msg); printWriter.flush(); } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { //调用发送消息方法 sendMsg(); } } //接受线程 class Accept extends Thread{ private Socket socket; //与客户端对应的socket public Accept(Socket socket){ this.socket = socket; } //接受消息 public void acceptMsg(){ try( //字符输入流 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream())) ) { while (true){ System.out.println("客户端说:"+bufferedReader.readLine()); } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { acceptMsg(); } } //主线程,启动服务端 public class ChatSocketServer { public static void main(String[] args) { try(ServerSocket serverSocket = new ServerSocket(8888)) { //监听客户端 System.out.println("服务端启动,正在监听客户端……"); Socket socket = serverSocket.accept(); System.out.println("连接成功!^_^"); new Send(socket).start(); //启动发送线程 new Accept(socket).start(); //启动接受线程 }catch (Exception e){ e.printStackTrace(); } } }
注意:主线程的作用就是去启动接受和发送线程,一旦主线程任务结束,主线程就死亡。主线程一旦死亡,try-with-resource就会将try括号中能关闭的流全部关闭,所以不能将Socket监听写在小括号内。可以写在小括号外面。
3.4.2 创建客户端
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; //接受消息线程 class ClientAccept extends Thread{ private Socket socket; public ClientAccept(Socket socket){ this.socket = socket; } public void acceptMsg(){ try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()))) { while (true){ System.out.println("服务端说:"+bufferedReader.readLine()); } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { acceptMsg(); } } //发送消息线程 class ClientSend extends Thread{ private Socket socket; public ClientSend(Socket socket){ this.socket = socket; } public void sendMsg(){ try( //创建键盘输入对象 Scanner scanner = new Scanner(System.in); //创建向客户端发送消息的输出流对象 PrintWriter printWriter = new PrintWriter(this.socket.getOutputStream()) ) { while (true){ String msg = scanner.nextLine(); printWriter.println(msg); printWriter.flush(); } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { sendMsg(); } } //主线程 public class ChatSocketClient { public static void main(String[] args) throws IOException { try { Socket socket = new Socket("127.0.0.1",8888); new ClientSend(socket).start(); //启动线程 new ClientAccept(socket).start(); }catch (Exception e){ e.printStackTrace(); } } }
这样就实现了客户端和服务器之间可以连续发送消息,而且消息的数量没有限制。
3.4.3 优化点对点
客户端和服务端的区别只是在连接的时候区分,当客户端和服务器连接成功之后没有区别了。
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; //发送线程 class TCPSend extends Thread{ private Socket socket; private Scanner scanner; public TCPSend(Socket socket,Scanner scanner){ this.scanner = scanner; this.socket = socket; } public void send(){ try(PrintWriter printWriter = new PrintWriter(socket.getOutputStream())) { while (true){ String s = scanner.nextLine(); printWriter.println(s); printWriter.flush(); } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { send(); } } //接受线程 class TCPAccept extends Thread{ private Socket socket; public TCPAccept(Socket socket){ this.socket = socket; } public void accept(){ try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()))) { while (true){ System.out.println(bufferedReader.readLine()); } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { accept(); } } public class GoodTCP { public static void main(String[] args) { ServerSocket serverSocket = null; Socket socket = null; try { //键盘输入,根据键盘输入决定启动的是客户端还是服务端 System.out.println("输入'服务端'启动服务端;输入’客户端‘启动客户端"); Scanner scanner = new Scanner(System.in); String s = scanner.nextLine(); if ("服务端".equals(s)){ System.out.println("正在启动服务器,请稍后O(∩_∩)O"); serverSocket = new ServerSocket(8888); System.out.println("正在监听8888端口"); socket = serverSocket.accept(); } if ("客户端".equals(s)){ socket = new Socket("127.0.0.1",8888); System.out.println("客户端启动成功!O(∩_∩)O"); } //启动线程 new TCPSend(socket,scanner).start(); new TCPAccept(socket).start(); }catch (Exception e){ e.printStackTrace(); }finally { if (serverSocket!=null){ try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } } }