Java基础入门--第十三章--网络编程

13.1 网络编程

13.1.1 网络通信协议

通过计算机网络可以实现多台计算机的连接,但是不同计算机的操作系统和硬件体系结构不同,为了提供通信支持,位于同一个网络中的计算机在进行连接和通信时必须遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交互。网络通信协议有很多种,例如,网络层的IP(InternetProtocol,网际互联协议),传输层的TCP(TransmissionControlProtocol,传输控制协议)和UDP(UserDatagramProtocol,用户数据报协议),应用层的FTP、HTTP、SMTP等。其中,TCP和IP是网络通信的主要协议,它们定义了计算机与外部设备进行通信时使用的规则。本章所学的网络编程知识主要基于TCP/IP中的内容。TCP/IP(又称TCP/IP协议簇)是一组用于实现网络互连的通信协议,其名称来源于该协议簇中两个重要的协议(TCP和IP)。基于TCP/IP的网络参考模型将网络分成4个层次,如图13-1所示在图13-1中,TCP/IP中的4个层次从最下层到最上层依次是链路层、网络层、传输层和应用层各层分别负责不同的通信功能。

  • 链路层。也称数据链路层或网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆或其他传输媒介有关的物理接口细节。
  • 网络层。也称网络互联层,是TCP/IP的核心,它主要用于将传输的数据分组,将分组数据发送到目标计算机或者网络。网络层对TCP/IP网络中的硬件资源进行标识。
  • 传输层。在TCP/IP网络中,不同的计算机之间进行通信时,数据的传输是由传输层控制的,包括数据要发往的目的主机及应用程序、数据的质量控制等。TCP/IP网络中最常用的传输协议一一TCP和UDP就应用于这一层。传输层通常以TCP或UDP来控制端点到端点的通信。用于通信的端点由Socket定义,而Socket由IP地址和端口号组成。
  • 应用层。主要负责应用程序的通信功能。大多数基于Internet的协议都被看作TCP/IP的应用层协议,如HTTP、FTP、SMTP、Telnet等。
    在这里插入图片描述

13.1.2 TCP与UDP

协议是定义的通信规则。在图13-1所示的基于TCP/IP的网络参考模型中,传输层的两个重要的高级协议分别是TCP和UDP。

1.TCP
TCP是面向连接的通信协议,即在传输数据前先在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠、无差错的数据传输。在TCP连接中必须明确客户端与服务器端,由客户端向服务器端发出连接请求。每次连接的创建都需要经过三次握手:第一次握手,客户端向服务器端发出连接请求,等待服务器端确认;第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求;第三次握手,客户端向服务器端发送确认信息,确认连接。TCP连接的整个交互过程如图13-2所示。因为TCP拥有面向连接的特性,所以它可以保证传输数据的安全性,是一个被广泛采用的协议。例如,在下载文件时,如果数据接收不完整,将会导致文件数据丢失而不能被打开,因此,下载文件时必须采用TCP。
在这里插入图片描述
2.UDP
UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另一台计算机发送数据时,发送端不会确认接收端是否存在就会发出数据;同样,接收端在收到数据时,也不会向发送端反馈是否收到数据。UDP的交互过程如图13-3所示。
在这里插入图片描述
由于UDP协议消耗资源小,通信效率高,所以UDP协议通常都会用于音频、视频和普通数据的传输,例如视频会议使用UDP协议,因为这种情况即使偶尔丢失一两个数据报,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,因为UDP具有面向无连接性,不能保证数据的完整性,所以在传输重要数据时不建议使用UDP协议。

13.1.3 IP地址和端口号

在网络编程中,如果一台计算机要和另一台计算机进行通信,需要通过IP地址建立连接,并通过端口号找到对应的服务,以此进行数据的交互。本节将分别对IP地址和端口号进行讲解。

1.IP地址
互联网上的每一台终端设备都有一个唯一标识,网络中的请求可以根据这个标识找到具体的计算机,这个唯一标识就是IP地址。
目前,IP地址广泛使用的版本是IPv4,它用4字节大小的二进制数表示,如00001010000000000000000000000001。因为二进制形式表示的IP地址非常不便于记忆,所以通常会将IP地址写成十进制的形式,一字节用一个十进制数字(0~255)表示,数字间用点符号(.)分开,如127.0.0.1。在Windows操作系统中,用户可以在命令行通过ipconfig命令查看本机的IP地址,具体如图13-4所示。
在这里插入图片描述
IP地址包括网络地址和主机地址两部分。其中,网络地址部分表示IP地址属于互联网的哪一个网络,是网络的地址编码;主机地址部分表示其属于该网络中的哪一台主机,是网络中一个主机的地址编码。二者是主从关系。IP地址根据网络地址和主机地址的范围分为5类,各类地址可使用的IP地址数量不同。IP地址分类及地址范围如表所示。

地址分类地址范围
A类地址1.0.0.1~126.255.255.254
B类地址128.0.0.1~191.255.255.254
C类地址192.0.0.1~223.255.255.254
D类地址224.0.0.0~239.255.255.255
E类地址240.0.0.0~255.255.255.255

在表中,可以发现没有127.X.X.X的地址,因为它是保留地址,用作循环测试。在开发中经常使用127.0.0.1表示本机的IP地址,例如,使用ping127.0.0.1命令测试本机TCP/IP是否正常。

2.端口号
在计算机中,端口号就是一个服务所占用的端口(port)的唯一标识。如果把计算机看作一座大楼,IP地址就相当于大楼的地址,端口号就是房间号。IP地址需要和端口号结合起来使用,这类似于快递小哥必须通过大楼地址和房间号才能准确找到收件人。网络中的请求需要通过IP地址找到计算机。同时,一台计算机上会同时运行很多个服务,不同的服务会占用不同的端口,计算机根据端口号把不同的请求分配给不同的服务。例如,计算机同时打开了微信和QQ,朋友通过微信发送了一条消息,请求到达计算机时,接收请求的是微信服务而不是QQ服务,就是因为不同的服务有不同的端口号,接收请求根据IP地址和端口号就可以准确地找到接收它的服务了。端口号是用16位的二进制数表示的,将其转换为十进制数后,取值范围是0~65535,其中,0~1023的端口号由操作系统的网络服务使用。例如,HTTP服务的端口号为80,Telnet服务的端口号为21,FTP服务的端口号为23。因此,当用户编写通信程序时,应选择一个大于1023的数作为端口号,从而避免服务端口号被操作系统占用的情况发生。
在这里插入图片描述

13.1.4 InetAddress类

Java提供了与IP地址相关的InetAddress类,该类用于封装一个IP地址。它还提供了一系列与IP地址相关的方法,下表列举了InetAddress类的常用方法。

方法声明功能描述
InetAddress getByName(String host)通过给定的主机名获取InetAddress对象的IP地址
InetAddress getByAddress(byte[] addr)通过存放在字节数组中的IP地址返回一个InetAddress对象
InetAddress getLocalHost()获取本地主机的IP地址
byte[] getAddress()获取本对象的IP地址,并存放在字节数组中
String getHostAddress()获取字符串格式的原始IP地址
String getHostName()获取IP地址的主机名。如果是本机,则是计算机名;如果不是本机,则是主机名;如果没有域名,则是IP地址
Boolean isReachable(int timeout)判断地址是否可以到达,同时指定超时时间
import java.net.InetAddress;

public class Example01 {
    public static void main(String[] args) throws Exception {
        InetAddress localAddress = InetAddress.getLocalHost();
        InetAddress remoteAddress = InetAddress.getByName("www.itcast.cn");
        System.out.println("本机的IP地址:" + localAddress.getHostAddress());
        System.out.println("www.itcast.cn的IP地址:"+remoteAddress.getHostAddress());
        System.out.println("3秒是否可达主机名为www.itcast.cn的ip地址:"+remoteAddress.isReachable(3000));
    }
}

第4行代码通过调用getLocalHost()方法获取表示本地主机的InetAddress对象。第5行代码通过调用getByName()方法取得远程InetAddress。第6行代码获取本地主机的IP地址并输出。第7、8行代码获取主机名为www.itcast.cn的主机的IP地址。第9、10行代码判断3s内是否可到达主机名为www.itcast.cn的主机的IP地址。
在这里插入图片描述

13.1.5 URL编程

URL(UniformResourceLocator)是统一资源定位器,它表示互联网上某一资源的地址。互联网上的资源包括HTML文件、图像文件、音频文件、视频文件等,只要按照URL规则定义某个资源,互联网上的程序就可以通过URL访问它。也就是说,通过URL访问互联网时,浏览器或其他程序通过解析给定的URL就可以在互联网上查找到相应的文件或资源。实际上,用户上网时在浏览器地址栏中输人的网址就是一个URL。URL的基本结构分为5部分,具体格式如下:

传输协议://主机名:端口号/文件名#引用

URL基本格式中每个部分的含义如下:
(1)传输协议:指访问资源使用的协议名,如HTTP、FTP等。
(2)主机名:指资源所在的计算机名称。主机名可以是IP地址,也可以是计算机的名称或域名。
(3)端口号:指服务使用的端口号。
(4)文件名:指访问的文件名称,包括该文件的完整路径。在HTTP中,有一个默认的文件名index.html,因此下列两个地址是等价的:

http://java.sun.com
http://java.sun.com/index.html

(5)引用:指资源内部的某个参考点,如http://java.sun.com/index.html#pagel。
以上就是URL的基本结构的5部分。需要注意的是,对于一个URL,并不要求必须包含所有的5部分。
Java中定义了URL类,用于访问网络上的资源,URL类是java.lang.Object类的直接子类。URL类中定义了一些常用方法,利用这些方法可以得到URL位置本身的数据,或者将URL对象转换成表示URL位置的字符串。URL类的常用方法如下表所示。

方法声明功能描述
public URL(String spec)throws MalformedURLException根据指定的地址实例化URL对象
public URL(String protocol,String host,int port,String file) throws MalformedURLException实例化URL对象,并指定协议、主机名、端口号和文件名
public URLConnection openConnection()throws IOException创建URLConnection对象
public final InputStream openStream()throws IOException创建输入流
import java.io.InputStream;
import java.net.URL;
import java.util.Scanner;

public class Example01 {
    public static void main(String[] args) throws Exception {
        URL url = new URL("http", "www.itcast.cn", 80, "/subject/uidszly/index.html");
        InputStream input = url.openStream();
        Scanner scan = new Scanner(input);
        scan.useDelimiter("\n");        //用于设置Scanner对象的分隔符。
        while(scan.hasNext()) {
            System.out.println(scan.next());
        }
    }
}

在这里插入图片描述
这个url内容已经加载不出来

13.2 TCP通信

在JDK中提供了两个用于实现TCP程序的类:一个是ServerSocket类,用于表示服务器端;另一个是Socket类,用于表示客户端。通信时,首先要创建代表服务器端的ServerSocket对象,创建该对象相当于开启一个服务,此服务会等待客户端的连接;然后创建代表客户端的Socket对象,使用该对象向服务器端发出连接请求,服务器端响应请求后,两者才建立连接,开始通信。ServerSocket类和Socket类通信的过程如图13-8所示。
在这里插入图片描述

13.2.1 ServerSocket类

编写服务器端程序需要使用ServerSocket类。ServerSocket类在java.net包中,java.net.ServerSocket继承java.lang.Object类。ServerSocket类的主要作用是接收客户端的连接请求。通过查阅API文档可知,ServerSocket类提供了多种构造方法,具体如表所示。

构造方法功能描述
ServerSocket()通过该构造方法创建的ServerSocket对象不与任何端口绑定,这样的ServerSocket对象创建的服务器端没有监听任何端口,不能直接使用,还需要继续调用bind(SocketAddressendpoint)方法将其绑定到指定的端口上,才可以正常使用
ServerSocket(intport)该构造方法的作用是以端口port创建ServerSocket对象,并等待客户端的连接请求
ServerSocket(int port,intbacklog)该构造方法在第二个构造方法的基础上增加了backlog参数,该参数用于指定最大连接数,即可以同时连接的客户端数量
ServerSocket(int port,int backlog,InetAddressbindAddr)该构造方法在第三个构造方法的基础上增加了bindAddr参数,该参数用于指定相关的IP地址
Socket accept()该方法用于等待客户端的连接。在客户端连接之前会一直处于阻塞状态;如果有客户端连接,就会返回一个与之对应的Socket对象
InetAddress getInetAddress()该方法用于返回一个InetAddress对象,该对象中封装了ServerSocket对象绑定的IP地址
boolean isClosed()该方法用于判断ServerSocket对象是否为关闭状态。如果是关闭状态,则返回true;反之则返回false
void bind(SocketAddress endpoint)该方法用于将ServerSocket对象绑定到指定的IP地址和端口号,其中endpoint参数封装了IP地址和端口号

13.2.2 Socket类

Socket类在java.net包中定义,java.net.Socket继承java.lang.Object类。Socket类用于编写客户端程序,用户通过创建一个Socket对象建立与服务器的连接。Socket类的构造方法如表所示。

方法声明功能描述
Socket()使用该构造方法,在创建Socket对象时,并没有指定IP地址和端口号,也就意味着只创建了客户端对象,并没有连接任何服务器
Socket(String host,int port)该构造方法用于在客户端以指定的主机名和端口号创建一个Socket对象,并向服务器端发出连接请求
Socket(InetAddress address,int port)创建一个流套接字,并将其连接到指定IP地址的指定端口
Socket(InetAddress address,int port,boolean stream)该构造方法在使用上与第二个构造方法类似,但IP地址由address指定
int getPort()该方法用于获取Socket对象与服务器端连接的端口号
InetAddress getLocalAddress()该方法用于获取Socket对象绑定的本地IP地址,并将IP地址封装成InetAddress类型的对象返回
InetAddress getInetAddress()该方法用于获取创建Socket对象时指定的服务器的IP地址
void close()该方法用于关闭Socket连接,结束本次通信。在关闭Socket连接之前,应将与Socket连接相关的所有的输人输出流全部关闭,这是因为一个良好的程序应该在执行完毕时释放所有的资源
InputStream getInputStream()该方法返回一个InputStream类型的输人流对象。如果该输人流对象是由服务器端的ServerSocket返回的,就用于读取客户端发送的数据;反之,用于读取服务器端发送的数据
OutputStream getOutputStream()该方法返回一个OutputStream类型的输出流对象,如果该输出流对象是由服务器端的ServerSocket返回的,就用于向客户端发送数据;反之,用于向服务器端发送数据

上中的getInputStream()方法和getOutputStream()方法分别用于获取输入流和输出流。当服务器端和客户端建立连接后,数据以1/O流的形式进行交互,从而实现通信。服务器端和客户端的数据传输如图13-9所示。
在这里插入图片描述

13.2.3 简单的TCP通信

下面通过一个TCP通信的案例进一步介绍这两个类的用法。要实现TCP通信,需要创建一个服务器端程序和一个客户端程序。

import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws Exception {
        Socket client =null;    //声明Socket对象
        OutputStream os=null; //声明OutputStream对象
        //创建ServerSocket对象并指定端口号(7788)
        ServerSocket serverSocket=new ServerSocket(7788);
        System.out.println("服务器正在运行,等待与客户端连接");
        client=serverSocket.accept();   //程序阻塞,等待客户端连接
        os=client.getOutputStream(); //获取客户端的输出流
        System.out.println("开始与客户端交互数据");
        //当客户端连接到服务器端时,向客户端输出数据
        os.write(("北京欢迎你!").getBytes());
        Thread.sleep(5000);     //模拟执行其他功能占用的时间
        System.out.println("结束与客户端交互数据");
        os.close();
        client.close();
    }
}

第6、7行代码分别声明了一个Socket对象和一个OutputStream对象,一个Socket对象表示一个客户端。第9行代码创建了一个ServerSocket对象代表服务器端,并指定端口号为7788。第11行代码使用ServerSocket类的对象client调用accept()方法等待客户端连接。第12行代码使用ServerSocket类的对象client调用ServerSocket类的getOutputStream()方法获取客户端的输出流。第15行代码当客户端连接到服务器端时使用输出流向客户端输出数据。第16行代码调用Thread类的sleep()方法使线程休眠5000ms,用于模拟执行其他功能占用的时间。最后,在第18、19行代码中分别调用OutputStream类与Socket类的close()方法关闭OutputStream对象与Socket对象。
在这里插入图片描述
控制台输出了“服务器正在运行,等待与客户端连接”,并且控制台中的光标一直在闪动,这是因为accept()方法在执行时发生阻塞,直到客户端连接成功后才会结束这种阻塞状态。

接下来编写客户端程序,并介绍如何通过客户端访问服务器端。

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) throws Exception {
        Socket client = null;                               //声明Socket对象
        client = new Socket("localhost", 7878);           //指定连接的主机端口号
        BufferedReader buf = null;      //声明BufferedReader对象,用于接收信息
        buf = new BufferedReader(
                new InputStreamReader(
                        client.getInputStream()              //取得客户端的输入流
                )
        );
        String str = buf.readLine();                        //读取信息
        System.out.println("服务器端输出内容:" + str);
        client.close();                                        //关闭Socket
        buf.close();                                            //关闭输入流
    }
}

在这里插入图片描述
为什么?
因为我的端口被占用了
是因为在一台计算机中,一个端口号只能被一个应用程序占用。当用户编写的UDP程序使用的端口号已经被其他应用程序占用时,就会出现这种情况。遇到这种情况时,可以在命令行窗口输入netstat-ano命令查看当前计算机端口的占用情况,netstat-ano命令的运行结果如图13-20所示。
在这里插入图片描述
在图中,显示了所有正在运行的应用程序及它们占用的端口号。要解决端口号占用的问题,只需关掉占用端口号的应用程序或者使用一个未被占用的端口号重新运行程序即可。
在这里插入图片描述
但是我还是没成功,明天再修改下。

13.2.4 多线程的TCP网络程序

当客户端程序向服务器端程序发送请求时,服务器端程序会结束阻塞状态,完成程序的运行。实际上,很多服务器端程序支持多线程,允许多个客户端同时访问,例如,门户网站可以被多个客户端同时访问。多个客户端访问同一个服务器端的过程如图13-13所示。在图13-13中,服务器端为每个客户端创建一个对应的Socket,并且开启一个新的线程使两个Socket建立专线进行通信。下面根据图13-13所示的通信方式对文件13-3的服务器端程序进行修改,修改后的服务器端程序如下所示。
在这里插入图片描述

13.3 UDP通信

数据传输时,如果相较于数据传输的性能更看重数据传输的完整性、可控制性和可靠性时,TCP无疑是最合适的选择;反之,在语音通话和视频通话这种更看重数据传输的性能的应用场景时,UDP是更合适的选择。UDP是一种面向无连接的协议,因此在通信时发送端和接收端不用建立连接。UDP通信的过程就像是货运公司在两个码头间发送货物一样。在码头发送和接收货物时都需要使用集装箱来装载货物,UDP通信也是一样,发送和接收的数据也需要使用“集装箱”进行打包,为此JDK中提供了一个DatagramPacket类,该类的实例对象就相当于一个集装箱,用于封装UDP通信中发送或者接收的数据。DatagramPacket的作用就如同是“集装箱”,可以将发送端或者接收端的数据封装起来。然而运输货物只有“集装箱”是不够的,还需要有码头。在程序中需要实现通信只有DatagramPacket数据包也同样不行,为此JDK中提供的一个DatagramSocket类。DatagramSocket类的作用就类似于码头,使用这个类的实例对象就可以发送和接收DatagramPacket数据包。通过DatagramPacket类和DatagramSocket类传输数据的过程如图13-15所示。
在这里插入图片描述

13.3.1 DatagramPacket类

DatagramPacket类用于封装UDP通信中发送或者接收的数据,DatagramPacket类的对象也称为数据报对象。利用UDP通信时,发送端使用DatagramPacket类将数据打包,即用DatagramPacket类创建一个数据报对象,这个数据报对象包含需要传输的数据、数据报的长度、IP地址和端口号等信息。要创建一个DatagramPacket对象,首先需要了解它的构造方法。在创建发送端和接收端的DatagramPacket对象时,使用的构造方法有所不同,接收端的构造方法只需要接收一个字节数组作为参数,用于存放接收到的数据;而发送端的构造方法不但要接收存放发送数据的字节数组,还需要指定发送端的IP地址和端口号。DatagramPacket类的构造方法如表所示。

方法声明功能描述
InetAddress getAddress()该方法用于返回发送端或者接收端的IP地址。如果是发送端的DatagramPacket对象,就返回接收端的IP地址;反之,就返回发送端的IP地址
int getPort()该方法用于返回发送端或者接收端的端口号。如果是发送端的DatagramPacket对象,就返回接收端的端口号;反之,就返回发送端的端口号
byte[]getData()该方法用于返回接收或者发送的数据。如果是发送端的DatagramPacket对象,就返回发送的数据;反之,就返回接收的数据
int getLength()该方法用于返回接收或者发送的数据的长度。如果是发送端的DatagramPacket对象,就返回发送的数据的长度;反之,就返回接收的数据的长度
利用DatagramPacket类的4个常用方法可以获取发送或者接收的DatagramPacket数据报中的信息。

13.3.2 DatagramSocket类

DatagramSocket类用于在发送主机中建立数据报通信方式,提出发送请求,实现数据报的发送与接收。在创建发送端和接收端的DatagramSocket对象时,使用的构造方法也有所不同。DatagramSocket类的构造方法如表所示。

DatagramSocket类的构造方法

方法声明功能描述
DatagramSocket()该构造方法用于创建发送端的DatagramSocket对象。该构造方法在创建DatagramSocket对象时并没有指定端口号,系统会分配一个没有被其他网络程序使用的端口号
DatagramSocket(int port)该构造方法既可以创建接收端的DatagramSocket对象,又可以创建发送端的DatagramSocket对象。在创建接收端的DatagramSocket对象时,必须指定一个端口号,这样就可以监听指定的端口
DatagramSocket(int port,InetAddress addr)该构造方法用于在有多个IP地址的当前主机上创建一个以addr为指定IP地址、以port为指定端口号的数据报连接

DatagramSocket类的常用方法

方法声明功能描述
void receive(DatagramPacket p)该方法用于接收数据,并将接收到的数据保存到DatagramPacket数据报中。在接收到数据之前,receive()方法会一直处于阻塞状态;只有当接收到数据报时,该方法才会返回
void send(DatagramPacket p)该方法用于发送DatagramPacket数据报,将数据报中包含的报文发送到p指定的IP地址的主机
void setSoTimeout(int timeout)设置传输数据时超时时间为timeout
void close()关闭数据报连接

需要注意的是,由于UDP连接是不可靠的通信方式,所以调用receive()方法时不一定能接收到数据。为了防止线程死循环,应该调用setSoTimeout()方法设置超时参数timeout。另外,receive()方法和send()方法都可能产生输人输出异常,因此都可能抛出IOException。

13.3.3 简单的UDP通信

前面讲解了DatagramPacket类和DatagramSocket类的相关知识。下面对UDP通信中数据报的发送与接收过程进行详细讲解。UDP通信中数据报的发送过程如下:
(1)创建一个用于发送数据报的DatagramPacket对象,使其包含如下信息:
要发送的数据。
数据报分组的长度。
发送目的地的主机IP地址和端口号。
(2)在指定的或可用的本机端口创建DatagramSocket对象。
(3)调用DatagramSocket对象的send()方法,以DatagramPacket对象为参数发送数据报。

UDP通信中数据报的接收过程如下:
(1)创建一个用于接收数据报的DatagramSocket对象,其中包含空白数据缓冲区和指定数据报分组的长度。(2)在指定的或可用的本机端口创建DatagramSocket对象。
(3)调用DatagramSocket对象的receive()方法,以DatagramPacket对象为参数接收数据报,接收到的信息如下:
收到的数据报分组。
发送端主机的IP地址。
发送端主机的端口号。

要实现UDP通信,需要创建一个发送端程序和一个接收端程序。在通信时,只有接收端程序先运行,才能避免发送端发送数据时找不到接收端而造成数据丢失的问题。因此,首先需要完成接收端程序的编写。

// 接收端程序
public class Receiver {
    public static void main(String[] args) throws Exception {
        byte[] buf = new byte[1024]; // 创建一个字节数组,用于接收数据
        // 定义一个DatagramSocket对象,端口号为8954
        DatagramSocket ds = new DatagramSocket(8954);
        // 定义一个DatagramPacket对象,用于接收数据
        DatagramPacket dp = new DatagramPacket(buf, buf.length);
        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();                 		 // 关闭数据报连接
    }
}

创建了一个接收端程序,用于接收发送端发送的数据。程序运行之后,接收端程序已经打开了监听端口,等待服务器端向客户端发送信息。其中,第5行代码定义了一个用于接收数据的字节数组,长度为1024。第7行代码定义了一个DatagramSocket对象,并指定此接收端的端口号为8954。第9行代码定义了一个DatagramPacket对象,在对象初始化时传人了buf数组,用于接收数据。第11行代码通过DatagramPacket对象ds调用receive()方法,用于接收数据。如果没有接收到数据,程序会处于阻塞状态,等待数据的接收;如果接收到数据,DatagramSocket对象会将数据填充到DatagramPacket对象中。第16、17行代码通过调用DatagramPacket的相关方法获取接收到的数据的内容、长度、发送端的IP地址和端口号等信息。第19行代码关闭数据报连接。
在这里插入图片描述
从上图可以看出,运行后,程序一直处于阻塞状态,这是因为DatagramSocket的receive()方法在等待接收发送端发送过来的数据,只有接收到发送端发送的数据,该方法才会结束阻塞状态,程序才能继续向下执行。实现了接收端程序之后,接下来编写发送端程序,用于给接收端发送数据。

//发送端程序
public class Sender {
    public static void main(String[] args) throws Exception {
        // 创建一个DatagramSocket对象
        DatagramSocket ds = new DatagramSocket(3000);
        String str = "hello world";   	// 要发送的数据
        byte[] arr = str.getBytes(); 	//将定义的字符串转为字节数组
         /*
 		 创建一个要发送的数据报,数据报包括发送的数据,
           数据的长度,接收端的IP地址以及端口号
 		*/
        DatagramPacket dp = new DatagramPacket(arr, arr.length,
                InetAddress.getByName("localhost"), 8954);
        System.out.println("发送信息");
        ds.send(dp); 					// 发送数据
        ds.close();  					// 释放资源
    }
}

在这里插入图片描述
运行发送端程序,接收端程序就会收到发送端程序发送的数据而结束阻塞状态,并输出接收的数据,如图下所示。
在这里插入图片描述

13.3.4 多线程的UDP网络程序

上述实现了发送端程序和接收端程序。当接收端程序处在阻塞状态下,运行发送端程序时,接收端程序会因为收到发送端发送的数据而结束阻塞状态,完成程序运行。实际上,发送端可以无限发送数据,接收端也可以一直接收数据。例如,聊天程序发送端可以一直发送消息,接收端也可以一直接收消息,因此发送端和接收端都是多线程的。接下来通过一个案例演示如何使用UDP通信方式实现多线程的UDP网络程序。

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class Example02 {
    public static void main(String[] args) {
        new Receive().start();
        new Send().start();
    }
}

class Receive extends Thread {
    public void run() {
        try {
            //创建socket相当于创建码头
            DatagramSocket socket = new DatagramSocket(6666);
            //创建packet相当于创建集装箱
            DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
            while(true) {
                socket.receive(packet);//接收货物
                byte[] arr = packet.getData();
                int len = packet.getLength();
                String ip = packet.getAddress().getHostAddress();
                System.out.println(ip + ":" + new String(arr,0,len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class Send extends Thread {
    public void run() {
        try {
            //创建socket相当于创建码头
            DatagramSocket socket = new DatagramSocket();
            Scanner sc = new Scanner(System.in);
            while(true) {
                String str = sc.nextLine();
                if("quit".equals(str))
                    break;
                DatagramPacket packet = new DatagramPacket(str.getBytes(),
                        str.getBytes().length, InetAddress.getByName
                        ("127.0.0.1"), 6666);
                socket.send(packet);//发货
            }
            socket.close();
        }  catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

首先简要介绍了网络的基础知识,包括基于TCP/IP的网络参考模型的4个层次、UDP与TCP、IP地址和端口号、InetAddress类和URL编程;
然后讲解了TCP程序设计中的ServerSocket类和Socket类,并通过两个案例实现了简单的TCP通信和多线程的TCP通信;
最后着重介绍了UDP程序设计中的DatagramPacket类和DatagramSocket类,并通过两个案例实现了简单的UDP通信和多线程的UDP通信。

  • 30
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值