Java Socket编程

Java Socket编程,URI/URL/URNInetAddressInetSocketAddress、TCP_Socket、UDP_Socket

URI、URL、URN

URI、URL、URN,这三者有什么区别
首先我们来看一下它们的全称:
1) URIUniform Resource Identifier,中文:统一资源标识符
2) URLUniform Resource Locator,中文:统一资源定位符
3) URNUniform Resource Name,中文:统一资源名称

其实从名字上就很容易看得出区别:
URI:强调标识,用于唯一标识一个网络资源
URL:强调定位,用于唯一定位一个网络资源
URN:强调名称,为资源提供持久的位置无关的名称

举个浅显的例子:
URI 就是一个人的个人信息,包括姓名、年龄、性别、住址、手机号等;
URL 就是特指这个人的住址;有了住址,我们就能够找到这个人;
URN 就是特指这个人的姓名;有了姓名,我们就能够讨论这个人;

从这个例子中可以看出,URL、URN 都是 URI 的子集,实际上也是这样的。如下图所示:
URI、URL、URN 区别

URL

例子,"https://www.zfl9.com/ss-redir.html?language=zh_CN#shadowsocks-libev"

URLConnection
java.net.URLConnection 是抽象类,它有两个子类,分别是:

  • java.net.HttpURLConnection:HTTP 连接,常用
  • java.net.JarURLConnection:Jar 连接(Jar 实际为 Zip 文件)

调用 java.net.URL 对象的 openConnection() 方法可以获取一个对应的 java.net.URLConnection 对象(可以多次调用 openConnection() 方法,每次返回的都是不同的连接对象,在多线程下载时,它很有用)。要注意的是,url.openConnection() 方法并不会打开一个实际的 url 连接,它只是创建了一个 URLConnection 对象而已,只有调用 URLConnection.connect() 方法后才开始建立实际的连接。对于 HttpURLConnection,建议使用 disconnect() 方法关闭连接(其它 url 连接可以使用 inputStream 的 close() 方法来关闭连接,当然 http 连接也可以)。

HttpURLConnection

简单例子:

多线程下载

多线程下载实现不难,只要理解了思路就行。这里提供的例子只支持最基本的多线程下载功能,不支持断点续传!

在讲多线程下载前,需要了解几个 HTTP 头部:

  • Content-Length: <length>:响应头,表示响应内容的长度,单位为 byte;
  • Content-Disposition: <disposition>:响应头,请求资源的描述信息,如文件名;
  • Range: bytes=<start>-<end>:请求头,表示客户端想获取指定范围的数据,多线程下载的关键;

默认情况下,服务器会返回整个文件内容(假设请求的是一个文件),这对于多线程下载实现是不利的。而添加 Range 请求头后,如果服务器支持,则返回 206 Partial Content 响应行,响应体为指定范围的数据,如果服务器不支持,则正常返回 200 OK,响应体为整个文件数据,因此在进行多线程下载前,务必检测响应状态码是否为 206!

那如何解决多线程同时写入文件呢?最简单的方式就是使用 java.io.RandomAccessFile 类,RandomAccessFile 支持随机存取文件,在多线程下载中,每个线程只需将获取到的数据写入到预创建的文件的给定范围内就可以了。

Range: bytes=<start>-<end> 中的 bytes 表示使用的单位为 byte,start、end 则为请求的数据范围。注意,start、end 是从 0 开始的,怎么理解呢?假如把整个要下载的文件存储在一个超大的字节数组中,那么 start、end 实际上就是该字节数组的某个下标,服务器则从 start 下标(含)开始,读取文件数据,直到 end 下标(含),最后将读取到的数据作为响应体,发送给客户端。因此,start 的最小值为 0,end 的最大值为 Content-Length - 1。当然也可以省略 end,如 Range: bytes=100- 表示获取从下标 100 到文件末尾的数据。

在进行多线程下载时,一般先在本地利用 RandomAccessFile 创建一个同大小的空文件。这个步骤一般只需发送 HEAD 请求,表示只获取响应头(因为我们只需要读取 Content-Length、Content-Disposition 头部),响应体没有意义。

然后根据文件大小、线程数量,计算每个线程需要下载的大小,当然,一般情况下很难整除,这时需要采取一个策略,即让最后一个线程多累一点,把剩下的也给下载了。我们把每个线程平均需要下载的大小叫做块大小,块大小通过 blockSize = totalSize / nthread 计算得到,totalSize 表示整个文件的大小,nthread 表示线程的数量。前 nthread - 1 个线程需要下载的大小都等于块大小,最后一个线程则需要下载剩余的全部数据。

那么该如何计算每个线程的 Range 范围呢?假设线程的编号从 0 开始,则线程 n 的 start 为 n * blockSize,线程 n(最后一个线程除外)的 end 为 n * blockSize + blockSize - 1,最后一个线程的 end 为 totalSize - 1。怎么算得?很简单,因为除最后一个线程外的线程需要下载的大小都是 blockSize,因此它们的 end 就是 start + blockSize - 1,而最后一个线程的 end 就是 totalSize - 1 了。

OK,实现思路已经很清晰了,现在开始写代码,实现一个简陋的多线程下载(简陋到基本输出都没写):

实现虽然简陋,但是核心功能还是没有问题的,使用:

InetAddress、InetSocketAddress

InetAddress:表示一个 32bit/128bit 的 IP 地址,有两个子类:Inet4AddressInet6Address
InetSocketAddress:除了包含 IP 地址外,还有 Port 端口号,用于 Socket 通信,是SocketAddress的子类;

InetAddress
因为构造方法的访问权限为包权限,因此不能直接在外部构造一个 InetAddress 对象,只能通过其 static 方法。

例子:

SocketAddress
SocketAddress是一个抽象类,并且类体为空,它的实现类是InetSocketAddress

TCP Socket

主要有两大类:ServerSocket服务端、Socket客户端

ServerSocket

Socket

例子:
单线程文件下载
Server.java

Client.java

运行结果

多线程文件下载
Server.java

Client.java

运行结果

线程池文件下载
Server.java

Client.java

运行结果

UDP Socket

主要用到两个类:DatagramSocketUDP Socket、DatagramPacketUDP 包;

DatagramSocket

DatagramPacket

例子:
Server.java

Client.java

运行结果