Java网络编程
1、InetAddress
JDK
中提供了一个 InetAdderss
类,该类用于封装一个 IP
地址,并提供了一系列与 IP
地址相关的方法,列
举了 InetAddress
类的一些常用方法。
InetAddress getByName(String host)
:
参数 host 表示指定的主机,该方法用于在给定主机名的情况下确定主机的 IP 地址。
InetAddress getLocalHost()
:
创建一个表示本地主机的 InetAddress 对象。
String getHostName()
得到 IP 地址的主机名,如果是本机则是计算机名,不是本机则是主机名,如果没有域名则是IP地址。
boolean isReachable(int timeout)
判断指定的时间内地址是否可以到达。
String getHostAddress()
得到字符串格式的原始 IP 地址。
列举了 InetAddress
的五个常用方法。其中,前两个方法用于获得该类的实例对象,第一个方法用于获得表示指
定主机的 InetAddress
对象,第二个方法用于获得表示本地的 InetAddress
对象。通过 InetAddress
对象便
可获取指定主机名,IP 地址等。
package com.example;
import java.net.InetAddress;
/**
* @author test
*/
public class Example01 {
public static void main(String[] args) throws Exception {
InetAddress localAddress=InetAddress.getLocalHost();
InetAddress remoteAddress=InetAddress.getByName("www.baidu.com");
System.out.println("本机的IP地址;"+localAddress.getHostAddress());
System.out.println("百度的IP地址:"+remoteAddress.getHostAddress());
System.out.println("3秒是否可达:"+remoteAddress.isReachable(3000));
System.out.println("百度的主机名为:"+remoteAddress.getHostName());
System.out.println("百度的原始IP地址为:"+remoteAddress.getHostAddress());
}
}
# 输出
本机的IP地址;192.168.149.1
百度的IP地址:39.156.66.18
3秒是否可达:true
百度的主机名为:www.baidu.com
百度的原始IP地址为:39.156.66.14
2、UDP通信-DatagramPacket
UDP 是一种面向无连接的协议,因此,在通信时发送端和接收端不用建立连接。UDP 通信的过程就像是货运公司
在两个码头间发送货物一样。在码头发送和接收货物时都需要使用集装箱来装载货物,UDP 通信也是一样,发送
和接收的数据也需要使用集装箱进行打包。为此 JDK 中提供了一个 DatagramPacket
类,该类的实例对象就相当
于一个集装箱,用于封装 UDP 通信中发送或者接收的数据。
想要创建一个 DatagramPacket
对象,首先需要了解一下它的构造方法。在创建发送端和接收端的
DatagramPacket
对象时,使用的构造方法有所不同,接收端的构造方法只需要接收一个字节数组来存放接收到
的数据,而发送端的构造方法不但要接收存放了发送数据的字节数组,还需要指定发送端 IP 地址和端口号。接下
来根据 API 文档的内容,对 DatagramPacket
的构造方法进行详细地讲解。
DatagramPacket(byte[] buf,int length)
使用该构造方法在创建 DatagramPacket 对象时,指定了封装数据的字节数组和数据的大小,没有指定IP地址和
端口号。很明显,这样的对象只能用于接收端,不能用于发送端。因为发送端一定要明确指出数据的目的地(IP地
址和端口号),而接收端不需要明确知道数据的来源,只需要接收到数据即可。
DatagramPacket(byte[]buf,int length,InetAddress addr,int port)
使用该构造方法在创建 DatagramPacket 对象时,不仅指定了封装数据的字节数组和数据的大小,还指定了数据
包的目标IP地址(addr)和端口号(port)。该对象通常用于发送端,因为在发送数据时必须指定接收端的IP地址和端
口号,就好像发送货物的集装箱上面必须标明接收人的地址一样。
DatagramPacket(byte[]buf,int offset,int length)
该构造方法与第一个构造方法类似,同样用于接收端,只不过在第一个构造方法的基础上,增加了一个offset参
数,该参数用于指定接收到的数据在放人buf缓冲数组时是从offset处开始的。
DatagramPacket(byte[]buf,int offset,int length,InetAddress addr,int port)
该构造方法与第二个构造方法类似,同样用于发送端,只不过在第二个构造方法的基础上,增加了一个offset参
数,该参数用于指定一个数组中发送数据的偏移量为offset,即从offset位置开始发送数据。
一些其它的方法:
InetAddress getAddress()
该方法用于返回发送端或者接收端的IP地址,如果是发送端的DatagramPacket对象,就返回接收端的IP地址,反
之,就返回发送端的IP地址。
int getPort()
该方法用于返回发送端或者接收端的端口号,如果是发送端的DatagramPacket对象,就返回接收端的端口号,反
之,就返回发送端的端口号。
byte[] getData()
该方法用于返回将要接收或者将要发送的数据,如果是发送端的 DatagramPacket对象,就返回将要发送的数据,
反之,就返回接收到的数据。
int getLength()
该方法用于返回接收或者将要发送数据的长度,如果是发送端的 DatagramPacket对象,就返回将要发送的数据长
度,反之,就返回接收到数据的长度。
列举了 DatagramPacket
类的四个常用方法及其功能,通过这四个方法,可以得到发送或者接收到的
DatagramPacket
数据包中的信息。
3、UDP通信-DatagramSocket
DatagramPacket
数据包的作用就如同是集装箱,可以将发送端或者接收端的数据封装起来。然而,运输货物只
有集装箱是不够的,还需要有码头。在程序中需要实现通信只有 DatagramPacket
数据包也同样不行,为此 JDK
提供了一个 DatagramSocket
类。DatagramSocket
类的作用就类似于码头,使用这个类的实例对象就可以发送
和接收 DatagramPacket
数据包。
在创建发送端和接收端的 DatagramSocket
对象时,使用的构造方法也有所不同,下面对 DatagramSocket
类中
常用的构造方法进行讲解。
DatagramSocket()
该构造方法用于创建发送端的 DatagramSocket 对象,在创建 DatagramSocket 对象时,并没有指定端口号,此
时,系统会分配一个没有被其他网络程序所使用的端口号。
DatagramSocket(int port)
该构造方法既可用于创建接收端的 DatagramSocket 对象,又可以创建发送端的DatagramSocket对象,在创建接
收端的DatagramSocket对象时,必须要指定一个端口号,这样就可以监听指定的端口。
DatagramSocket(int port,InetAddress addr)
使用该构造方法在创建 DatagramSocket 时,不仅指定了端口号,还指定了相关的IP地址,这种情况适用于计算
机上有多块网卡的情况,可以明确规定数据通过哪块网卡向外发送和接收哪块网卡的数据。由于计算机中针对不同
的网卡会分配不同的IP,因此在创建DatagramSocket对象时需要通过指定IP地址来确定使用哪块网卡进行通信。
对DatagramSocket类中的常用方法进行详细地讲解:
void receive(DatagramPacket p)
该方法用于将接收到的数据填充到DatagramPacket数据包中,在接收到数据之前会一直处于阻塞状态,只有当接
收到数据包时,该方法才会返回。
void send(DatagramPacket p)
该方法用于发送 DatagramPacket 数据包,发送的数据包中包含将要发送的数据、数据的长度、远程机的IP地址
和端口号。
void close()
关闭当前的Socket,通知驱动程序释放为这个Socket保留的资源。
4、UDP网络程序
讲解了 DatagramPacket
和 DatagramSocket
的作用,接下来通过一个案例来学习一下它们在程序中的具体用
法。要实现 UDP 通信需要创建一个发送端程序和一个接收端程序,很明显,在通信时只有接收端程序先运行,才
能避免因发送端发送的数据无法接收,而造成数据丢失。
接收端程序:
package com.example;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* @author test
*/
public class Example02 {
public static void main(String[] args) throws Exception {
// 创建一个长度为1024的字节数组,用于接收数据
byte[] buf = new byte[1024];
// 定义一个DatagramSocket对象,监听的端口号为8954
DatagramSocket ds = new DatagramSocket(8954);
// 定义一个DatagramPacket对象,用于接收数据
DatagramPacket dp = new DatagramPacket(buf, 1024);
System.out.println("等待接收数据...");
// 等待接收数据,如果没有数据则会阻塞
ds.receive(dp);
// 调用DatagramPacket的方法获得接收到的信息,包括数据的内容、长度、IP地址和端口号
String str = new String(dp.getData(), 0, dp.getLength()) + "from" +
dp.getAddress().getHostAddress() + ":" + dp.getPort();
// 打印接收到的信息
System.out.println(str);
// 释放资源
ds.close();
}
}
package com.example;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* @author test
*/
public class Example03 {
public static void main(String[] args) throws Exception{
// 创建一个DatagramSocket对象
DatagramSocket ds=new DatagramSocket(3000);
// 要发送的数据
String str="hello world";
DatagramPacket dp=new DatagramPacket(str.getBytes(),str.length(),
InetAddress.getByName("localhost"),8954);
System.out.println("发送信息");
//发送数据
ds.send(dp);
//释放资源
ds.close();
}
}
发送信息
等待接收数据...
hello worldfrom127.0.0.1:3000
需要注意的时,在创建发送端的 DatagramSocket 对象时,可以不指定端口号,而指定端口号的目的就是,为了
每次运行时接收端的 getPort() 方法返回值都是一致的,否则发送端的端口号由系统自动分配,接收端的getPort()
方法的返回值每次都不同。
5、TCP通信
TCP 通信同 UDP 通信一样,都能实现两台计算机之间的通信,通信的两端都需要创建 Socket 对象。区别在于,
UDP 中只有发送端和接收端,不区分客户端与服务器端,计算机之间可以任意地发送数据。而 TCP 通信是严格区
分客户端与服务器端的,在通信时,必须先由客户端去连接服务器端才能实现通信,服务器端不可以主动连接客户
端,并且服务器端程序需要事先启动,等待客户端的连接。
在 JDK 中提供了两个类用于实现 TCP 程序,一个是 ServerSocket
类,用于表示服务器端,一个是 Socket
类,
用于表示客户端。通信时,首先创建代表服务器端的 ServerSocket
对象,该对象相当于开启一个服务,并等待
客户端的连接,然后创建代表客户端的 Socket
对象向服务器端发出连接请求,服务器端响应请求,两者建立连
接开始通信。
5.1 TCP通信-ServerSocket
通过前面的学习知道,在开发 TCP 程序时,首先需要创建服务器端程序。JDK 的 java.net
包中提供了一个
ServerSocket
类,该类的实例对象可以实现一个服务器端的程序。通过查阅 API 文档可知,ServerSocket
类
提供了多种构造方法,接下来就对 ServerSocket 的构造方法进行逐一地讲解。
ServerSocket()
使用该构造方法在创建 ServerSocket 对象时并没有绑定端口号,这样的对象创建的服务器端没有监听任何端口,
不能直接使用,还需要继续调用 bind(Socket Address endpoint)
方法将其绑定到指定的端口号上,才可以正
常使用。
ServerSocket(int port)
使用该构造方法在创建 ServerSocket对象时,就可以将其绑定到一个指定的端口号上(参数port就是端口号)。端口
号可以指定为0,此时系统就会分配一个还没有被其他网络程序所使用的端口号。由于客户端需要根据指定的端口
号来访问服务器端程序,因此端口号随机分配的情况并不常用,通常都会让服务器端程序监听一个指定的端口号。
ServerSocket(int port,int backlog)
该构造方法就是在第二个构造方法的基础上,增加了一个 backlog 参数。该参数用于指定在服务器忙时,可以与
之保持连接请求的等待客户数量,如果没有指定这个参数,默认为50。
ServerSocket(int port,int backlog,Inet Address bindAddr)
该构造方法就是在第三个构造方法的基础上,还指定了相关的P地址,这种情况适用于计算机上有多块网卡和多个
IP的情况,我们可以明确规定ServerSocket在哪块网卡或IP地址上等待客户的连接请求。显然,对于一般只有一块
网卡的情况,就不用专门的指定了。
在以上介绍的构造方法中,第二个构造方法是最常使用的。了解了如何通过ServerSocket的构造方法创建对象,
接下来学习一下ServerSocket 的常用方法:
Socket accept()
该方法用于等待客户端的连接,在客户端连接之前一直处于阻塞状态,如果有客户端连接就会返回一个与之对应的
Socket对象。
InetAddress getInetAddress()
该方法用于返回一个 InetAddress 对象,该对象中封装了ServerSocket 绑定的 IP地址
boolean isClosed()
该方法用于判断 ServerSocket 对象是否为关闭状态,如果是关闭状态则返回true,反之则返回false。
void bind(SocketAddress endpoint)
该方法用于将 ServerSocket 对象绑定到指定的 IP地址和端口号,其中参数 endpoint 封装了IP地址和端口号。
ServerSocket对象负责监听某台计算机的某个端口号,在创建ServerSocket对象后,需要继续调用该对象的
accept()方法,接收来自客户端的请求。当执行了accept()方法之后,服务器端程序会发生阻塞,直到客户端发出
连接请求,accept()方法才会返回一个Socket对象用于和客户端实现通信,程序才能继续向下执行。
5.2 TCP通信-Socket
上一小节中讲解了 ServerSocket 对象可以实现服务端程序,但只实现服务器端程序还不能完成通信,此时还需要
一个客户端程序与之交互。为此 JDK 提供了一个 Socket
类,用于实现 TCP 客户端程序。通过查阅 API 文档可知
Socket 类同样提供了多种构造方法,接下来就对 Socket 的常用构造方法进行详细讲解。
Socket()
使用该构造方法在创建 Socket 对象时,并没有指定IP地址和端口号,也就意味着只创建了客户端对象,并没有去
连接任何服务器。通过该构造方法创建对象后还需调用 connect(SocketAddress endpoint)
方法,才能完成与
指定服务器端的连接,其中参数endpoint用于封装IP地址和端口号。
Socket(String host,int port)
使用该构造方法在创建Socket对象时,会根据参数去连接在指定地址和端口上运行的服务器程序,其中参数host
接收的是一个字符串类型的IP地址。
Socket(Inet Address address,int port)
该方法在使用上与第二个构造方法类似,参数address用于接收一个InetAddress类型的对象,该对象用于封装一
个IP地址。
在以上Socket的构造方法中,最常用的是第一个构造方法。了解了Socket 的构造方法,接下来学习一下Socket的
常用方法:
int getPort()
该方法返回一个int类型对象,该对象是Socket对象与服务器端连接的端口号。
InetAddress getLocalAddress()
该方法用于获取Socket对象绑定的本地IP地址,并将IP地址封装成 InetAddress类型的对象返回。
void close()
该方法用于关闭Socket连接,结束本次通信。在关闭Socket之前,应将与Socket相关的所有的输人输出流全部关
闭,这是因为一个良好的程序应该在执行完毕时释放所有的资源。
InputStream getInputStream()
该方法返回一个 InputStream 类型的输入流对象,如果该对象是由服务器端的Socket返回,就用于读取客户端发
送的数据,反之,用于读取服务器端发送的数据。
OutputStream getOutputStream()
该方法返回一个 OutputStream 类型的输出流对象,如果该对象是由服务器端的Socket返回,就用于向客户端发
送数据,反之,用于向服务器端发送数据。
其中 getInputStream()
和 getOutStream()
方法分别用于获取输人流和输出流。当客户端和服务端建立连接
后,数据是以IO流的形式进行交互的,从而实现通信。
6、TCP网络程序
要实现 TCP 通信需要创建一个服务器端程序和一个客户端程序。
服务端程序:
package com.example;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author test
*/
public class Example04 {
public static void main(String[] args) throws Exception {
new TcpServer().listen();
}
}
/**
* TCP服务端
*/
class TcpServer {
private static final int PORT = 7788;
public void listen() throws Exception {
ServerSocket serverSocket = new ServerSocket(PORT);
Socket client = serverSocket.accept();
// 获取客户端的输出流
OutputStream os = client.getOutputStream();
System.out.println("开始与客户端交互数据");
os.write(("Hello World!").getBytes());
Thread.sleep(5000);
System.out.println("结束与客户端交互数据");
os.close();
client.close();
}
}
客户端程序:
package com.example;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
/**
* @author test
*/
public class Example05 {
public static void main(String[] args) throws Exception {
new TcpClient().connect();
}
}
/**
* TCP客户端
*/
class TcpClient {
private static final int PORT = 7788;
public void connect() throws Exception {
Socket client = new Socket(InetAddress.getLocalHost(), PORT);
InputStream is = client.getInputStream();
byte[] buf = new byte[1024];
int len = is.read(buf);
System.out.println(new String(buf, 0, len));
client.close();
}
}
开始与客户端交互数据
结束与客户端交互数据
Hello World!
7、多线程的TCP网络程序
多个客户端访问同一个服务器端,服务器端为每个客户端创建一个对应的Socket,并且开启一个新的线程使两个
Socket建立专线进行通信。
客户端程序不变,服务器端程序:
package com.example;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author test
*/
public class Example06 {
public static void main(String[] args){
new TcpServer1().listen();
}
}
/**
* TCP服务端
*/
class TcpServer1 {
private static final int PORT = 7788;
public void listen() {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
final Socket client = serverSocket.accept();
new Thread(()->{
OutputStream os;
try {
os = client.getOutputStream();
System.out.println("开始与客户端交互数据");
os.write(("Hello World!").getBytes());
Thread.sleep(5000);
System.out.println("结束与客户端交互数据");
os.close();
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
# 多次调用输出
开始与客户端交互数据
结束与客户端交互数据
开始与客户端交互数据
结束与客户端交互数据
开始与客户端交互数据
结束与客户端交互数据
8、TCP案例-文件上传
目前大多数服务器都会提供文件上传的功能,由于文件上传需要数据的安全性和完整性,很明显需要使用 TCP 协
议来实现。接下来通过一个案例来实现图片上传的功能,首先编写服务器端程序,用来接收图片。
服务端:
package com.example;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author test
*/
public class Example07 {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(10001);
// 创建ServerSocket对象
while (true) {
// 调用accept()方法接收客户端请求,得到Socket对象
Socket s = serverSocket.accept();
// 每当和客户端建立Socket连接后,单独开启一个线程处理和客户端的交互
new Thread(new ServerThread(s)).start();
}
}
}
class ServerThread implements Runnable {
/**
* 持有一个Socket类型的属性
*/
private Socket socket;
/**
* 构造方法中把Socket对象作为实参传人
*
* @param socket
*/
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//获取客户端的 IP地址
String ip = socket.getInetAddress().getHostAddress();
int count = 1;
//上传图片个数
try {
InputStream in = socket.getInputStream();
// 创建上传图片目录的File对象
File parentFile = new File(".\\upload");
if (!parentFile.exists()) {
// 如果不存在,就创建这个目录
parentFile.mkdir();
}
// 把客户端的 IP地址作为上传文件的文件名
File file = new File(parentFile, ip + "(" + count + ").jpg");
while (file.exists()) {
// 如果文件名存在,则把 count++
file = new File(parentFile, ip + "(" + (count++) + ").jpg");
}
// 创建 FileOutputStream对象
FileOutputStream fos = new FileOutputStream(file);
// 定义一个字节数组
byte[] buf = new byte[1024];
// 定义一个int类型的变量len,初始值为0
int len = 0;
// 循环读取数据
while ((len = in.read(buf)) != -1) {
fos.write(buf, 0, len);
}
// 获取服务端的输出流
OutputStream out = socket.getOutputStream();
out.write("上传成功".getBytes());
// 上传成功后向客户端写出上传成功
fos.close();
// 关闭输出流对象
// 关闭Socket对象
socket.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
package com.example;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* @author test
*/
public class Example08 {
public static void main(String[] args) throws Exception {
// 创建客户端Socket
Socket socket = new Socket("127.0.0.1", 10001);
// 获取Socket的输出流对象
OutputStream out = socket.getOutputStream();
// 创建FileInputStream对象
FileInputStream fis = new FileInputStream("src.png");
// 定义一个字节数组
byte[] buf = new byte[1024];
// 定义一个int类型的变量 len
int len;
while ((len = fis.read(buf)) != -1) {
//循环读取数据
out.write(buf, 0, len);
}
// 关闭客户端输出流
socket.shutdownOutput();
// 获取Socket的输人流对象
InputStream in = socket.getInputStream();
// 定义一个字节数组
byte[] bufMsg = new byte[1024];
int num = in.read(bufMsg);
// 接收服务端的信息
String Msg = new String(bufMsg, 0, num);
System.out.println(Msg);
fis.close();
//关键输入流对象
socket.close();
//关闭 Socket对象
}
}