第十节 Java 网络编程
1. 网络编程基础
1.1 网络通信协议
在计算机网络中,连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交互。
TCP/IP协议(又称TCP/IP协议簇)是一组用于实现网络互联的通信协议。
基于TCP/IP协议参考模型的网络层次结构
(1)链路层:用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、双绞线提供的驱动
(2)网络层:网络层是整个TCP/IP协议的核心,它用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络
(3)运输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议
(4)应用层:主要为互联网中的各种网络应用提供服务
1.2 IP地址和端口号
IP作用:通过这个标识来寻找计算机
port端口号作用:用来标识一个应用程序的
举个例子:
一个邮件到达你的手中,会先通过你的地址找到你这个位置找到叫张三的这个人,这个位置就类似IP地址,但是可能这个地方有很多人都叫张三,我们再通过电话号码来确定具体是哪个张三的,这里面的张三类似应用程序,这个电话号码就类似端口号。
IP 地址的分类:
(1)A类地址:由第一段的网络地址和其余三段的主机地址组成,范围是1.0.0.0到127.255.255.255
(2)B类地址:由前两段网络地址和其余两段的主机地址组成,范围是128.0.0.0到191.255.255.255
(3)B类地址:由前三段网络地址和其余两段的主机地址组成,范围是192.0.0.0到223.255.255.255
(4)D类和E类为特殊地址。
其中127.0.0.1是一个本地回环地址,就是本机,可用来测试。我们可以利用ping 127.0.0.1来测试本机的TCP/IP协议是否正常
端口号是由2个字节表示的,它的取值范围是0~65535,其中0~1023之间的端口号用于一些知名的网络服务和应用,用户的普通应用程序需要使用1024以上端口号。
1.3 InetAddress
在JDK中提供了一个与IP地址有关的InetAddress类该类用于封装一个IP地址,并提供了一些列与IP地址相关的方法。
InetAdress类的常用方法:
方法声明 | 功能描述 |
---|---|
InetAdress getByName(String host) | 获取给定主机名的IP地址,host参数表示指定主机 |
InetAddress getLocalHost() | 获取本地主机地址 |
String getHostName() | 获取本地IP地址的主机名 |
boolean isReachable(int timeout) | 判断在限定的时间内指定的IP地址是否可以访问 |
String getHostAddress() | 获取字符串格式的原始IP地址 |
前两个方法是静态方法。
示例如下:
public class InetAdressDemo {
public static void main(String[] args) throws IOException {
//通过字符串获取InetAdress对象
InetAddress remoteAdress=InetAddress.getByName("www.baidu.com");
//获取本地InetAdress对象
InetAddress localadress=InetAddress.getLocalHost();
System.out.println("本机的IP地址为:"+localadress.getHostAddress());
System.out.println("本机的主机名为:"+localadress.getHostName());
System.out.println("判断百度在五秒内是否可以访问"+remoteAdress.isReachable(5000));
System.out.println("百度的IP地址为:"+remoteAdress.getHostAddress());
}
}
效果如下图:
1.4 UDP与TCP
(1)UDP
UDP称为用户数据报协议,是无连接通信协议,在数据进行传输时,数据的发送端和接收端不建立逻辑连接。优点:UDP协议消耗小,通信效率高,延迟小,因为这种情况即使丢失一两个数据包,也不会对接收结果影响太大。由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议用UDP
(2)TCP
TCP协议称为传输控制协议,是面向连接的通信协议,在数据传输之前要先建立逻辑连接,然后再传输数据,保证了可靠无差错的数据传输。每次建立连接都要经历“三次握手”。第一次握手,客户端向服务器端发出连接请求,等待服务器确认,第二次握手,服务器端向客户端返回一个回应,通知客户端收到了连接请求;第三次握手,客户端再次向服务器端发送确认信息,确认连接。因此TCP传输速度慢,但是传输的数据比较可靠。
2. UDP 通信
JDK中提供了一个DatagramPacket类,该类的实例就类似集装箱,用于封装UDP通信中发送或接收的数据。DatagramSocket类,这个类的作用类似于码头,使用这个类的实例对象可以发送和接收DatagramPacket数据报。
2.1 DatagramPacket
在接收端构造方法只需要接收一个字节数组来存放接收到的数据,而发送端的构造方法不但要接收存放了发送数据的字节数组,还需要指定发送端IP地址和端口号。
(1)DatagramPacket(byte[ ] buf,int length)
使用该构造方法在创建DatagramPacket对象时,指定了封装数据的字节数组和数据的大小,没有指定IP地址和端口号,很显然它只能用于接收端。
(2)DatagramPacket(byte[ ] buf,int offset,int length)
该构造方法与第一个构造方法类似也只用于接收端,多的offset参数用于指定一个数组中发送数据的偏移量,即从offset位置开始
(3)DatagramPacket(byte[ ] buf ,int length,InetAddress addr,int port)
使用该构造方法创建DatagramPacket对象时,指明了要发送的数据及数据大小,同时也指明了目标ip地址和端口号。该对象通常用在发送端。
(4)DatagramPacket(byte[ ] buf ,int offset,int length,InetAddress addr,int port)
与第三个类似,不过多了个offset,用来表示从offset位置开始发送数据
DatagramPacket类中常用方法:
方法声明 | 功能描述 |
---|---|
InetAddress getAddress() | 该方法用于返回发送端或者接收端的IP地址,如果是发送端的DatagramPacket对象,就返回接收端的IP地址;反之,返回发送端的IP地址 |
int getPort() | 该方法用于返回发送端或者接收端的端口号,如果是发送端的DatagramPacket对象,就返回接收端的端口号;反之,返回发送端的端口号 |
byte[ ] getData() | 该方法用于返回将要接收或者将要发送的数据,如果是发送端的DatagramPacket对象,就返回将要发送的数据;反之,返回接收到的数据 |
int getLength() | 该方法用于返回将要接收或者将要发送的数据的长度,如果是发送端的DatagramPacket对象,就返回将要发送的数据的长度;反之,返回接收到的数据的长度 |
2.2 DatagramSocket
用于创建发送端和接收端对象。
构造方法如下:
(1)DatagramSocket()
用于创建发送端的DatagramSocket对象,这里并没有指定端口号,系统会分配一个没有被其他网络程序所使用的端口号
(2)DatagramSocket(int port)
用于创建接收端DatagramSocket对象,也可以创建发送端的DatagramSocket对象,在创建接收端的时候必须要有端口号,用于监听指定端口
(3)DatagramSocket(int port,InetAddress addr)
不仅指定了端口号,还指定了ip地址,这个构造方法一般用于计算机有多块网卡的情况。
DatagramSocket类中的常用方法:
方法声明 | 功能描述 |
---|---|
void receive(DatagramPacket p) | 该方法用于接收DatagramPacket数据报,在接收到数据之前会一直处于阻塞状态,如果发送消息的长度比数据报长,则消息会被截取 |
void send(DatagramPacket p) | 该方法用于发送DatagramPacket数据报,发送的数据报中包含将要发送的数据、数据的长度、远程主机的IP地址和端口号 |
void close() | 关闭当前的Socket,通知驱动程序释放这个Socket保留的资源 |
UDP网络程序:
//接收端
public class UDPReceiver {
public static void main(String[] args) throws IOException {
//定义一个指定端口号为8900的接收端DatagramSocket对象
DatagramSocket server=new DatagramSocket(8900);
byte[] buf=new byte[1024];
//定义一个DatagramPacket数据报对象
DatagramPacket packet=new DatagramPacket(buf,buf.length);
System.out.println("等待接收数据.....");
while (true){
//等待接收数据
server.receive(packet);
String str=new String(packet.getData(),0,packet.getLength());
System.out.println(packet.getAddress()+":"+packet.getPort()+"发送消息"+str);
}
}
}
//发送端
public class UDPsend {
public static void main(String[] args) throws IOException {
//定义一个指定端口号为3000的发送端对象
DatagramSocket client=new DatagramSocket(3000);
//定义要发送的数据
String str="hello world";
//定义一个DatagramPacket数据报对象,封装发送端信息以及发送地址
DatagramPacket packet=new DatagramPacket(str.getBytes(),str.getBytes().length,InetAddress.getByName("localhost"),8900);
System.out.println("开始发送信息.....");
client.send(packet);
client.close();
}
}
效果如下:
在这里的DatagramPacket所能接收的数据大小是有限的,理论最大65507字节,但是实际要比这少的多,在许多平台最大时8K。
我们 可以使用netstat -anb 这个命令来查看端口号占用情况
小案例:聊天程序:
public class ChatRoom {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
System.out.print("请输入聊天服务当前启动端口号:");
int serverPort=sc.nextInt();
System.out.print("请输入聊天服务发送信息对象的目标端口号:");
int targetPort=sc.nextInt();
System.out.println("聊天系统初始化完成并启动!!!");
try{
//创建聊天程序收发平台DatagramSocket对象
DatagramSocket socket=new DatagramSocket(serverPort);
//分别启动信息接收端和发送端程序
new Thread(new ChatReceiver(socket),"接收服务").start();
new Thread(new ChatSend(socket,targetPort),"发送服务").start();
}catch (SocketException e){
e.printStackTrace();
}
}
}
public class ChatReceiver implements Runnable{
private DatagramSocket server;
public ChatReceiver(DatagramSocket server){
this.server=server;
}
@Override
public void run() {
try {
//创建DatagramPacket数据包接收对象
byte[] buf=new byte[1024];
DatagramPacket packet=new DatagramPacket(buf,buf.length);
while (true){
server.receive(packet);
//显示并打印聊天信息
String str=new String(packet.getData(),0,packet.getLength());
System.out.println("收到"+packet.getAddress()+":"+packet.getPort()+"发送的数据:"+str);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class ChatSend implements Runnable{
//聊天程序信息发送平台DatagramSocket对象
private DatagramSocket client;
private int targetPort;
public ChatSend(DatagramSocket client,int targetPort){
this.client=client;
this.targetPort=targetPort;
}
@Override
public void run() {
try {
//获取键盘输入对象
Scanner sc=new Scanner(System.in);
while (true){
String data=sc.nextLine();
//封装数据到DatagramPacket数据包发送对象中
byte[] buf=data.getBytes();
DatagramPacket packet=new DatagramPacket(buf,buf.length, InetAddress.getByName("127.0.0.255"),targetPort);
client.send(packet);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
3. TCP 通信
严格区分客户端和服务器端。在JDK 中提供两个用于实现TCP程序的类,一个是ServerSocket类,用于表示服务器端;另一个是Socket类用于表示客户端。必须保证服务器端先开开。
3.1 ServerSocket
JDK的java.net包中提供了一个ServerSocket类,该类的实例对象可以实现一个服务器端的程序。
ServerSocket的构造方法:
(1)ServerSocket()
使用该构造方法在创建ServerSocket对象时,并没有指定端口号,因此该对象不监听任何端口,不能直接使用,使用时还需要调用bind(SocketAddress endpoint)方法将其绑定到指定的端口号上
(2)ServerSocket(int port)
使用该构造方法在创建ServerSocket对象时,可以将其绑定到指定的端口号上。如果port参数值为0,此时系统就会分配一个未被其他程序占用的端口号。
(3)ServerSocket(int port,int backlog)
该构造方法中的backlog参数是用于指定在服务器忙时,可以与之保持连接请求的等待客户端数量,如果没有指定这个参数,默认为50.
(4)ServerSocket(int port,int backlog, InetAddress bindAddr)
适用于计算机有多个网卡。
ServerSocket的常用方法
方法声明 | 功能描述 |
---|---|
Socket accept() | 该方法用于等待客户端的连接,在客户端连接之前一直处于阻塞状态,如果有客户端连接就会返回一个与之对应的Socket对象 |
InetAddress getInetAddress() | 该方法用于返回一个InetAddress对象,该对象中封装了ServerSocket绑定的IP地址 |
boolean isClosed() | 该方法用于判断ServerSocket对象是否为关闭状态,如果是关闭状态则返回true,反之返回false |
void bind(SocketAddress endpoint) | 该方法用于将ServerSocket对象绑定到指定的IP地址和端口号,其中参数endpoint封装了IP地址和端口号 |
3.2 Socket
JDK提供了一个Socket类,用于实现TCP客户端程序
构造方法:
(1)Socket()
使用这个构造方法没有指定IP地址和端口号,就说明只是创建了客户端对象,没有连接任何服务器。通过该构造方法创建对象后还需要调用connect(SocketAddress endpoint)方法,才能完成与指定服务器端的连接。
(2)Socket(String host,int port)
该构造方法会根据参数去连接指定地址和端口上运行的服务器程序
(3)Socket(InetAddress address,int port)
address用于接收一个InetAddress类型的对象。与第二个构造方法类似
Socket常用方法:
方法声明 | 功能描述 |
---|---|
int getPort() | 返回此Socket连接的远程服务端的端口号 |
InetAddress getLocalAddress() | 用于获取Socket对象绑定的本地IP地址,并将IP地址封装成InetAddress类型的对象返回 |
void close() | 该方法用于关闭Socket连接,结束本次通信。在关闭Socket之前,应将与Socket相关的所有输出输入流全部关闭,这是因为一个良好的程序应该在执行完毕时释放所有资源 |
InputStream getInputStream() | 该方法返回一个InputStream类型的输入流对象。如果该对象是由服务器端的Socket返回,就用于读取客户端发送的数据;反之,用于读取服务器端发送的数据 |
OutputStream getOutputStream() | 该方法返回一个OutputStream类型的输出流对象。如果该对象是由服务器端的Socket返回,就用于向客户端发送数据;反之,用于向服务器端发送数据。 |
简单的TCP网络程序
public class TCPServer {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket=new ServerSocket(7788);
while (true){
//调用ServerSocket的accept()方法开始接收数据
Socket client=serverSocket.accept();
System.out.println("与客户端连接成功,开始进行数据交互!");
//获取客户端的输出流
OutputStream os=client.getOutputStream();
//当客户端连接到服务器端,向客户端输出数据
os.write(("服务器端向客户端做出响应!".getBytes()));
//模拟与客户端交互耗时
Thread.sleep(5000);
//关闭流和Socket连接
os.close();
client.close();
}
}
}
public class TCPClient {
public static void main(String[] args) throws IOException {
//创建一个Socket并连接到指定服务器
Socket client=new Socket(InetAddress.getLocalHost(),7788);
//获取服务器端返回的输入流并打印
InputStream is=client.getInputStream();
byte[] buf=new byte[1024];
int len=is.read(buf);
while (len!=-1){
System.out.println(new String(buf,0,len));
len=is.read(buf);
}
//关闭流
is.close();
client.close();
}
}
多线程的TCP网络程序
public class TCPClient {
public static void main(String[] args) throws IOException {
//创建一个Socket并连接到指定服务器
Socket client=new Socket(InetAddress.getLocalHost(),7788);
//获取服务器端返回的输入流并打印
InputStream is=client.getInputStream();
byte[] buf=new byte[1024];
int len=is.read(buf);
while (len!=-1){
System.out.println(new String(buf,0,len));
len=is.read(buf);
}
//关闭流
is.close();
client.close();
}
}
public class MultithreadingTCPServer {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket=new ServerSocket(7788);
while (true){
//调用ServerSocket的accept()方法开始接收数据
Socket client=serverSocket.accept();
Thread thread=new Thread(()->{
try {
//获取当前连接客户端的端口号
int port=client.getPort();
System.out.println("与端口号为"+port+"的客户端连接成功,开始进行数据交互!");
//获取客户端的输出流
OutputStream os=client.getOutputStream();
//当客户端连接到服务器端,向客户端输出数据
os.write(("服务器端向客户端做出响应!".getBytes()));
//模拟与客户端交互耗时
Thread.sleep(5000);
System.out.println("结束与客户端数据交互");
//关闭流和Socket连接
os.close();
client.close();
}catch (Exception e){
e.printStackTrace();
}
});
thread.start();
}
}
}
效果如下:
TCP实现文件的上传
public class ServerThread implements Runnable{
private Socket socket;
public ServerThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
//1.处理客户端请求,进行上传文件保存
String ip=socket.getInetAddress().getHostAddress();
int count=1;
try{
//创建图片上传保存目录
File parentFile=new File("E:\\upload\\");
if (!parentFile.exists()){
parentFile.mkdir();
}
//把客户端的IP地址作为文件上传的文件名
File file=new File(parentFile,ip+"("+count+").jpg");
while (file.exists()){
file=new File(parentFile,ip+"("+count++ +").jpg");
}
//通过客户端输入流读取上传图片写入到指定目录
InputStream in=socket.getInputStream();
FileOutputStream fos=new FileOutputStream(file);
byte[] buf=new byte[1024];
int len=0;
while ((len=in.read(buf))!=-1){
fos.write(buf,0,len);
}
//服务器端向客户端做出响应
OutputStream out=socket.getOutputStream();
out.write("上传成功".getBytes());
//关闭流和Socket连接
in.close();
fos.close();
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
public class UploadTCPServer {
public static void main(String[] args) throws IOException {
//创建指定端口号为10001的服务端ServerSocket对象
ServerSocket serverSocket=new ServerSocket(10001);
while (true){
//调用accept()方法持续接收客户端请求
Socket client=serverSocket.accept();
new Thread(new ServerThread(client)).start();
}
}
}
public class UploadTCPClient {
public static void main(String[] args) throws IOException {
Socket client=new Socket(InetAddress.getLocalHost(),10001);
//客户端向服务器端上传文件
OutputStream out=client.getOutputStream();
FileInputStream fis=new FileInputStream("E:\\image\\bg1.jpg");
byte[] buf=new byte[1024];
int len=0;
System.out.println("连接到服务器端,开始文件上传!");
while((len=fis.read(buf))!=-1){
out.write(buf,0,len);
}
//整个图片上传完成后,要及时关闭客户端输出流
client.shutdownOutput();
//客户端接收服务端的响应
InputStream is=client.getInputStream();
byte[] bufMsg=new byte[1024];
int len2=is.read(bufMsg);
while (len2!=-1){
System.out.println(new String(bufMsg,0,len2));
len2=is.read(bufMsg);
}
//关闭流和Socket连接
out.close();
is.close();
fis.close();
client.close();
}
}
这里需要特别注意:
shutDownOutput()方法,在这里非常的重要,因为服务器端程序在while循环中读写客户端发送的数据,当读取到-1时才会结束,如果在客户端不调用shutDownOutput()方法,服务器端就不会读到-1,而会一直执行循环,同时客户端读取服务器端数据的read(byte[ ])方法也是一个阻塞方法,这样服务器端和客户端就进入了死锁状态。
4. URL 编程
使用示例:
public class Demo2 {
public static void main(String[] args) throws IOException {
//关键使用步骤:
//1. 先准备一个URL类的对象 u
URL url = new URL("http://www.4399.com");
//2. 打开服务器连接,得到连接对象 conn
URLConnection conn = url.openConnection();
//3. 获取加载数据的字节输入流 is
InputStream is = conn.getInputStream();
//4. 将is装饰为能一次读取一行的字符输入流 br
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//5. 加载一行数据
String text = br.readLine();
//6. 显示
System.out.println(text);
//5. 加载一行数据
String text2 = br.readLine();
//6. 显示
System.out.println(text2);
//5. 加载一行数据
String text3 = br.readLine();
//6. 显示
System.out.println(text3);
//5. 加载一行数据
String text4 = br.readLine();
//6. 显示
System.out.println(text4);
//5. 加载一行数据
String text5 = br.readLine();
//6. 显示
System.out.println(text5);
//5. 加载一行数据
String text6 = br.readLine();
//6. 显示
System.out.println(text6);
//7. 释放资源
br.close();
}
}