现代的应用程序都离不开网络,网络编程是非常重要的技术。Kotlin标准库网络编程源自于Java提供java.net包,其中包含了网络编程所需要的最基础一些类和接口。这些类和接口面向两个不同的层次:
1.基于Socket的低层次网络编程。Socket采用TCP、UDP等协议,这些协议属于低层次的通信协议,编程过程比较复杂。
2.基于URL的高层次网络编程。URL采用HTTP和HTTPS这些属于高层次的通信协议,相对低层编程过程比较容易。
所谓低层次网络编程并不等于它功能不强大。恰恰相反,正因为层次低,Socket编程与基于URL的高层次网络编程比较,能够提供更强大的功能和更灵活的控制,但是要更复杂一些。
本章会介绍基于Socket的低层次网络编程和基于URL的高层次网络编程,以及数据交换格式。
网络编程需要程序员掌握一下基础的网络知识,这一节先介绍一些网络基础知识。
23.1.1 网络结构
首先了解一下网络结构,网络结构是网络的构建方式,目前流行的有客户端服务器结构网络和对等结构网络。
1.客户端服务器结构网络
客户端服务器(Client Server,缩写C/S)结构网络,是一种主从结构网络。如图23-1所示,服务器一般处于等待状态,如果有客户端请求,服务器响应请求建立连接提供服务。服务器是被动的,有点像在餐厅吃饭时候的服务员。而客户端是主动的,像在餐厅吃饭的顾客。
事实上,生活中很多网络服务都采用这种结构。例如:Web服务、文件传输服务和邮件服务等。虽然它们存在的目的不一样,但基本结构是一样的。这种网络结构与设备类型无关,服务器不一定是电脑,也可能是手机等移动设备。
2.对等结构网络
对等结构网络也叫点对点网络(Peer to Peer,缩写P2P),每个节点之间是对等的。它们如图23-2所示,每个节点既是服务器又是客户端,这种结构有点像吃自助餐。
对等结构网络分布范围比较小。通常在一间办公室或一个家庭内,因此它非常适合于移动设备间的网络通讯,网络链路层是由蓝牙和WiFi实现。
23.1.2 TCP/IP协议
网络通信会用到协议,其中TCP/IP协议是非常重要的。TCP/IP协议是由IP和TCP两个协议构成的,IP(Internet Protocol)协议是一种低级的路由协议,它将数据拆分成许多小的数据包中,并通过网络将它们发送到某一特定地址,但无法保证都所有包都抵达目的地,也不能保证包的顺序。
由于IP协议传输数据的不安全性,网络通信时还需要TCP协议,传输控制协议(Transmission Control Protocol,TCP)是一种高层次的协议,面向连接的可靠数据传输协议,如果有些数据包没有收到会重发,并对数据包内容准确性检查并保证数据包顺序,所以该协议保证数据包能够安全地按照发送时顺序送达目的地。
23.1.3 IP地址
为实现网络中不同计算机之间的通信,每台计算机都必须有一个与众不同的标识,这就是IP地址,TCP/IP使用IP地址来标识源地址和目的地址。最初所有的IP地址都是32位数字构成,由4个8位的二进制数组成,每8位之间用圆点隔开,如:192.168.1.1,这种类型的地址通过IPv4指定。而现在有一种新的地址模式称为IPv6,IPv6使用128位数字表示一个地址,分为8个16位块。尽管IPv6比IPv4有很多优势,但是由于习惯的问题,很多设备还是采用IPv4。不过Kotlin语言同时指出IPv4和IPv6。
在IPv4地址模式中IP地址分为A、B、C、D和E等5类。
o A类地址用于大型网络,地址范围:1.0.0.1~126.155.255.254。
o B类地址用于中型网络,地址范围:128.0.0.1~191.255.255.254。
o C类地址用于小规模网络,192.0.0.1~223.255.255.254。
o D类地址用于多目的地信息的传输和作为备用。
o E类地址保留仅作实验和开发用。
另外,有时还会用到一个特殊的IP地址127.0.0.1,127.0.0.1称为回送地址,指本机。主要用于网络软件测试以及本地机进程间通信,使用回送地址发送数据,不进行任何网络传输,只在本机进程间通信。
23.1.4 端口
一个IP地址标识这一台计算机,每一台计算机又有很多网络通信程序在运行,提供网络服务或进行通信,这就需要不同的端口进行通信。如果把IP地址比作电话号码,那么端口就是分机号码,进行网络通信时不仅要指定IP地址,还要指定端口号。
TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535。小于1024的端口号保留给预定义的服务,如HTTP是80,FTP是21,Telnet是23,Email是25等,除非要和那些服务进行通信,否则不应该使用小于1024的端口。
23.2 TCP Socket低层次网络编程
TCP/IP协议的传输层有两种传输协议:TCP(传输控制协议)和 UDP(用户数据报协议)。TCP是面向连接的可靠数据传输协议。TCP通信过程类似于打电话,电话接通后双方才能通话,在挂断电话之前,电话一直占线。TCP连接一旦建立起来,一直占用,直到关闭连接。另外,TCP为了保证数据的正确性,会重发一切没有收到的数据,还会对进行数据内容进行验证,并保证数据传输的正确顺序。因此TCP协议对系统资源的要求较多。
基于TCP Socket编程很有代表性,先介绍TCP Socket编程。
23.2.1 TCP Socket通信概述
Socket是网络上的两个程序,通过一个双向的通信连接,实现数据的交换。这个双向链路的一端称为一个Socket。Socket通常用来实现客户端和服务端的连接。Socket是TCP/IP协议的一个十分流行的编程接口,一个Socket由一个IP地址和一个端口号唯一确定,一旦建立连接Socket还会包含本机和远程主机的IP地址和远端口号,如图23-3所示,Socket是成对出现的。
23.2.2 TCP Socket通信过程
使用Socket进行C/S结构编程,通信过程如图23-4所示。
服务器端监听某个端口是否有连接请求,服务器端程序处于阻塞状态,直到客户端向服务器端发出连接请求,服务器端接收客户端请求,服务器会响应请求,处理请求,然后将结果应答给客户端,这样就会建立连接。一旦连接建立起来,通过Socket可以获得输入输出流对象。借助于输入输出流对象就可以实现服务器与客户端的通信,最后不要忘记关闭Socket和释放一些资源(包括:关闭输入输出流)。
23.2.3 Socket类
java.net包为TCP Socket编程提供了两个核心类:Socket和ServerSocket,分别用来表示双向连接的客户端和服务器端。
本节先介绍一下Socket类,Socket常用的构造函数有:
o Socket(address: InetAddress!, port: Int)。创建Socket对象,并指定远程主机IP地址和端口号。
o Socket(address: InetAddress!, port: Int, localAddr: InetAddress!,
localPort: Int)。创建Socket对象,并指定远程主机IP地址和端口号,以及本机的IP地址(localAddr)和端口号(localPort)。
o Socket(host: String!, port: Int)。创建Socket对象,并指定远程主机名和端口号,IP地址为null,null表示回送地址,即127.0.0.1。
o Socket(host: String!, port: Int, localAddr: InetAddress!, localPort:
Int)。创建Socket对象,并指定远程主机和端口号,以及本机的IP地址(localAddr)和端口号(localPort)。host主机名,IP地址为null,null表示回送地址,即127.0.0.1。
Socket其他的常用函数和属性有:
o getInputStream()函数。通过此Socket返回输入流对象。
o getOutputStream()函数。通过此Socket返回输出流对象。
o port: Int属性。返回Socket连接到的远程端口。
o localPort属性。返回Socket绑定到的本地端口。
o inetAddress属性。返回Socket连接的地址。
o localAddress属性。返回Socket绑定的本地地址。
o isClosed属性。返回Socket是否处于关闭状态。
o isConnected属性。返回Socket是否处于连接状态。
o close()函数。关闭Socket。
23.2.4 ServerSocket类
ServerSocket类常用的构造函数:
o ServerSocket(port: Int, maxQueue: Int)。创建绑定到特定端口的服务器Socket。maxQueue设置连接的请求最大队列长度,如果队列满时,则拒绝该连接。默认值是50。
o ServerSocket(port: Int)。创建绑定到特定端口的服务器Socket。连接的请求最大队列长度是50。
ServerSocket其他的常用函数和属性有:
o getInputStream()函数。通过此Socket返回输入流对象。
o getOutputStream()函数。通过此Socket返回输出流对象。
o isClosed属性。返回Socket是否处于关闭状态。
o isConnected属性。返回Socket是否处于连接状态。
o accept()函数。侦听并接收到Socket的连接。此函数在建立连接之前一直阻塞。
ServerSocket类本身不能直接获得I/O流对象,而是通过accept()函数返回Socket对象,通过Socket对象取得I/O流对象,进行网络通信。此外,ServerSocket也实现了AutoCloseable接口,通过自动资源管理技术关闭ServerSocket。
23.2.5 案例:文件上传工具
基于TCP Socket编程比较复杂,先从一个简单的文件上传工具案例介绍TCP Socket编程基本流程。上传过程是一个单向Socket通信过程,如图23-5所示,客户端通过文件输入流读取文件,然后从Socket获得输出流写入数据,写入数据完成上传成功,客户端任务完成。服务器端从Socket获得输入流,然后写入文件输出流,写入数据完成上传成功,服务器端任务完成。
下面看看案例服务器端UploadServer代码如下:
//代码文件:chapter23/src/com/a51work6/section2/UploadServer.kt
package com.a51work6.section2
import java.io.BufferedInputStream
import java.io.FileOutputStream
import java.net.ServerSocket
fun main(args: Array) {
println(“服务器端运行…”)
ServerSocket(8080).use { server -> ①
server.accept().use { socket-> ②
BufferedInputStream(socket.getInputStream()).use { sin -> ③
FileOutputStream("./TestDir/subDir/coco2dxcplus.jpg").use {fout -> ④
sin.copyTo(fout)
println(“接收完成!”)
}
}
}
}
}
上述代码第①行ServerSocket(8080)语句创建ServerSocket对象,并监听本机的8080端口,这是当前线程还没有阻塞,调用代码第②行的server.accept()才会阻塞当前线程,等待客户端请求。
代码第③行socket.getInputStream()是从socket对象中获得输入流对象,代码第④行是文件输出流。上面输入输出代码读者可以参考第22章,这里不再赘述。
再看看案例客户端UploadClient代码如下:
//代码文件:chapter23/src/com/a51work6/section2/UploadClient.kt
package com.a51work6.section2
import java.io.BufferedOutputStream
import java.io.FileInputStream
import java.net.Socket
fun main(args: Array) {
println(“客户端运行…”)
Socket(“127.0.0.1”,8080).use { socket -> ①
BufferedOutputStream(socket.getOutputStream()).use { sout -> ②
FileInputStream("./TestDir/coco2dxcplus.jpg").use { fin ->
fin.copyTo(sout)
println(“上传成功!”)
}
}
}
}
上述代码第①行Socket(“127.0.0.1”,
8080)是创建Socket,指定远程主机的IP地址和端口号。代码第②行socket.getOutputStream()是从socket对象获得输出流。
UDP(用户数据报协议)就像日常生活中的邮件投递,是不能保证可靠地寄到目的地。UDP是无连接的,对系统资源的要求较少,UDP可能丢包不保证数据顺序。但是对于网络游戏和在线视频等要求传输快、实时性高、质量可稍差一点的数据传输,UDP还是非常不错的。
UDP Socket网络编程比TCP Socket编程简单多,UDP是无连接协议,不需要像TCP一样监听端口,建立连接,然后才能进行通信。
23.3.1 DatagramSocket类
java.net包中提供了两个类DatagramSocket和DatagramPacket,它们用来支持UDP通信。这一节先介绍一下DatagramSocket类,DatagramSocket用于在程序之间建立传送数据报的通信连接。
先来看一下DatagramSocket常用的构造函数:
o DatagramSocket()。创建数据报DatagramSocket对象,并将其绑定到本地主机上任何可用的端口。
o DatagramSocket(port: Int)。创建数据报DatagramSocket对象,并将其绑定到本地主机上的指定端口。
o DatagramSocket(port: Int, laddr: InetAddress!)。创建数据报DatagramSocket对象,并将其绑定到指定的本地地址。
DatagramSocket其他的常用函数和属性有:
o send(p: DatagramPacket!)。从发送数据报包。
o receive(p: DatagramPacket!)。接收数据报包。
o port属性。返回DatagramSocket连接到的远程端口。
o localPort属性。返回DatagramSocket绑定到的本地端口。
o inetAddress属性。返回DatagramSocket连接的地址。
o localAddress属性。返回DatagramSocket绑定的本地地址。
o isClosed属性。返回DatagramSocket是否处于关闭状态。
o val isConnected: Boolean属性。返回DatagramSocket是否处于连接状态。
o close()函数。关闭Socket。
DatagramSocket也实现了AutoCloseable接口,通过自动资源管理技术关闭DatagramSocket。
23.3.2 DatagramPacket类
DatagramPacket用来表示数据报包,是数据传输的载体。DatagramPacket实现无连接数据包投递服务,每投递数据包仅根据该包中信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达,不保证包都能到达目的。
下面看一下DatagramPacket的构造函数:
o DatagramPacket(buf: ByteArray!, length: Int)。构造数据报包,buf包数据,length是接收包数据的长度。
o DatagramPacket(buf: ByteArray!, length: Int, address: InetAddress!,
port: Int)。构造数据报包,包发送到指定主机上的指定端口号。
o DatagramPacket(buf: ByteArray!, offset: Int, length: Int)。构造数据报包,offset是buf字节数组的偏移量。
o DatagramPacket(buf: ByteArray!, offset: Int, length: Int, address:
InetAddress!, port: Int)。构造数据报包,包发送到指定主机上的指定端口号。
DatagramPacket常用属性:
o address。返回发往或接收该数据报包相关的主机的IP地址。属性类型是InetAddress。
o data。返回数据报包中的数据。属性类型是ByteArray。
o length。返回发送或接收到的数据的长度。属性类型是Int。
o offset。返回发送或接收到的数据的偏移量。属性类型是Int。
o port。返回发往或接收该数据报包相关的主机的端口号。属性类型是Int。
23.3.3 案例:文件上传工具
使用UDP Socket将23.2.5节文件上传工具重新实现一下。
下面看看案例服务器端UploadServer代码如下:
//代码文件:chapter23/src/com/a51work6/section3/UploadServer.kt
package com.a51work6.section3
…
fun main(args: Array) {
println("服务器端运行...")
DatagramSocket(8080).use { socket-> ①
FileOutputStream("./TestDir/subDir/coco2dxcplus.jpg").use {
fout ->
BufferedOutputStream(fout).use { out ->
// 准备一个缓冲区
val buffer =ByteArray(1024)
//循环接收数据报包
while (true) {
// 创建数据报包对象,用来接收数据
val packet =DatagramPacket(buffer, buffer.size)
// 接收数据报包
socket.receive(packet)
// 接收数据长度
val len = packet.length
if (len == 3) { ②
// 获得结束标志
val flag =String(buffer, 0, 3) ③
// 判断结束标志,如果是bye结束接收
if (flag =="bye") {
break
}
}
// 写入数据到文件输出流
out.write(buffer, 0,len)
}
println("接收完成!")
}
}
}
}
上述代码第①行DatagramSocket(8080)是创建DatagramSocket对象,并指定端口8080,作为服务器一般应该明确指定绑定的端口。
与TCP Socket不同UDP Socket无法知道哪些数据包已经是最后一个了,因此需要发送方发出一个特殊的数据包,包中包含了一些特殊标志。代码第③行~第④行是取出并判断这个标志。
再看看案例客户端UploadClient代码如下:
//代码文件:chapter23/src/com/a51work6/section3/UploadClient.kt
package com.a51work6.section3
…
fun main(args: Array) {
println(“客户端运行…”)
DatagramSocket().use { socket -> ①
FileInputStream("./TestDir/coco2dxcplus.jpg").use { fin ->
BufferedInputStream(fin).use{ input ->
// 创建远程主机IP地址对象
val address =InetAddress.getByName("localhost")
// 准备一个缓冲区
val buffer =ByteArray(1024)
// 首次从文件流中读取数据
var len =input.read(buffer)
while (len != -1) {
// 创建数据报包对象
val packet =DatagramPacket(buffer, len, address, 8080)
// 发送数据报包
socket.send(packet)
// 再次从文件流中读取数据
len =input.read(buffer)
}
// 创建数据报对象
val packet =DatagramPacket("bye".toByteArray(), 3, address, 8080)
// 发送结束标志
socket.send(packet) ②
println("上传完成!")
}
}
}
}
上述是上传文件客户端,发送数据不会堵塞线程,因此没有使用子线程。代码第①行DatagramSocket()是创建DatagramSocket对象,由系统分配可以使用的端口,作为客户端DatagramSocket对象经常自己不指定了,而是有系统分配。
代码第②行是发送结束标志,这个结束标志是字符串bye,服务器端接收到这个字符串则结束接收数据包。
数据交换格式就像两个人在聊天一样,采用彼此都能听得懂的语言,你来我往,其中的语言就相当于通信中的数据交换格式。有时候,为了防止聊天被人偷听,可以采用暗语。同理,计算机程序之间也可以通过数据加密技术防止“偷听”。
数据交换格式主要分为纯文本格式、XML格式和JSON格式,其中纯文本格式是一种简单的、无格式的数据交换方式。
例如,为了告诉别人一些事情,我会写下如图23-6所示的留言条。
留言条有一定的格式,共有4部分:称谓、内容、落款和时间,如图23-7所示。
如果用纯文本格式描述留言条,可以按照如下的形式:
“云龙同学”,“你好!\n今天上午,我到你家来想向你借一本《小学生常用成语词典》。可是不巧,你不在。我准备晚上6时再来借书。请你在家里等我,谢谢!”,“关东升”,“2012年12月08日”
留言条中的4部分数据按照顺序存放,各个部分之间用逗号分隔。数据量小的时候,可以采用这种格式。但是随着数据量的增加,问题也会暴露出来,可能会搞乱它们的顺序,如果各个数据部分能有描述信息就好了。而XML格式和JSON格式可以带有描述信息,它们叫做“自描述的”结构化文档。
将上面的留言条写成XML格式,具体如下:
将上面的留言条写成JSON格式,具体如下:
{to:“云龙同学”,conent:“你好!\n今天上午,我到你家来想向你借一本《小学生常用成语词典》。可是不巧,你不在。我准备晚上6时再来借书。请你在家里等我,谢谢!”,from:“关东升”,date:“2012年12月08日”}
数据放置在大括号{}之中,每个数据项目之前都有一个描述名字(如to等),描述名字和数据项目之间用冒号(:)分开。
可以发现,一般来讲,JSON所用的字节数要比XML少,这也是很多人喜欢采用JSON格式的主要原因,因此JSON也被称为“轻量级”的数据交换格式。接下来,重点介绍JSON数据交换格式。
23.4.1 JSON文档结构
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。所谓轻量级,是与XML文档结构相比而言的,描述项目的字符少,所以描述相同数据所需的字符个数要少,那么传输速度就会提高,而流量却会减少。
如果留言条采用JSON描述,可以设计成下面的样子:
{“to”:“云龙同学”,
“conent”: “你好!\n今天上午,我到你家来想向你借一本《小学生常用成语词典》。可是不巧,你不在。我准备晚上6时再来借书。请你在家里等我,谢谢!”,
“from”: “关东升”,
“date”: “2012年12月08日”}
由于Web和移动平台开发对流量的要求是要尽可能少,对速度的要求是要尽可能快,而轻量级的数据交换格式JSON就成为理想的数据交换格式。
构成JSON文档的两种结构为对象和数组。对象是“名称-值”对集合,它类似于Java中Map类型,而数组是一连串元素的集合。
对象是一个无序的“名称/值”对集合,一个对象以左括号({)开始,右括号(})结束。每个“名称”后跟一个冒号(:),“名称-值”对之间使用逗号(,)分隔。JSON对象的语法表如图23-8所示。
下面是一个JSON对象的例子:
{
“name”:“a.htm”,
“size”:345,
“saved”:true
}
数组是值的有序集合,以左中括号([)开始,右中括号(])结束,值之间使用逗号(,)分隔。JSON数组的语法表如图23-9所示。
下面是一个JSON数组的例子:
[“text”,“html”,“css”]
在数组中,值可以是双引号括起来的字符串、数值、true、false、null、对象或者数组,而且这些结构可以嵌套。数组中值的JSON语法结构如图23-10所示。
23.4.2 使用第三方JSON库
由于目前Kotlin官方没有提供JSON编码和解码所需要的类库,所以需要使用第三方JSON库,笔者推荐Klaxon库,Klaxon库纯Kotlin代码编写的,最重要的是不依赖于其他第三方库,支持Gradle配置很容易添加到现有项目中。读者可以在https://github.com/cbeust/klaxon查看帮助和下载源代码。
添加Klaxon库到现有项目中,这需要创建IntelliJ
IDEA+Gradle项目,项目创建完成后在打开build.gradle文件,修改文件内容如下:
group ‘com.51work6’
version ‘1.0-SNAPSHOT’
buildscript {
ext.kotlin_version = ‘1.1.51’
repositories {
mavenCentral()
}
dependencies {
classpath"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: ‘java’
apply plugin: ‘kotlin’
sourceCompatibility = 1.8
repositories {
mavenCentral()
jcenter() ①
}
dependencies {
compile “org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version”
compile ‘com.beust:klaxon: 2.0.0’ ②
testCompile group: ‘junit’, name:‘junit’, version: ‘4.12’
}
compileKotlin {
kotlinOptions.jvmTarget =“1.8”
}
compileTestKotlin {
kotlinOptions.jvmTarget =“1.8”
}
在build.gradle文件中添加代码第①行和代码第②行。其中代码第①行添加Gradle仓库jcenter(),Klaxon库在jcenter仓库。代码第②行添加依赖关系。
23.4.3 JSON数据编码和解码
JSON和XML真正在进行数据交换时候,它们存在的形式就是一个很长的字符串,这个字符串在网络中传输或者存储于磁盘等介质中。在传输和存储之前需要把JSON对象转换成为字符串才能传输和存储,这个过程称之为“编码”过程。接收方需要将接收到的字符串转换成为JSON对象,这个过程称之为“解码”过程。编码和解码过程就像发电报时发送方把语言变成能够传输的符号,而接收时要将符号转换成为能够看懂的语言。
下面具体介绍一下JSON数据编码和解码过程。
1.编码
如果想获得如下这样JSON字符串:
{“name”:“tony”,“age”:30,“sex”:false,“a”:[1,3]}
应该如何实现编码过程,参考代码如下:
val jsonObject = json { ①
obj(“name” to “tony”, “age” to 30) ②
}
jsonObject.put(“sex”, false) ③
val list = listOf(1, 3) ④
val jsonArray1 = json { ⑤
//array(1, 3) ⑥
array(list) ⑦
}
jsonObject.put(“a”, jsonArray1)⑧
val jsonArray2 = json { ⑨
array(jsonArray1) ⑩
}
// 编码完成
println(jsonObject.toJsonString(prettyPrint = true)) ⑪
println(jsonArray2.toJsonString()) ⑫
运行结果如下:
{
“name”:
“tony”,
“age”:
30,
“sex”:
false,
“a”:
[1, 3]
}
[[1,3]]
上述代码第①行是通过json函数创建JSON对象,代码第②行指定JSON对象内容,JSON对象是一种Map结构,其中"name" to
"tony"是一个键值对。json函数可以不仅可以创建JSON对象,还可以创建JSON数组,代码第⑤行和代码第⑨行都是创建JSON数组,json函数中定义了4个函数:
obj(vararg args: Pair
array(args: List
JsonArray
另外,向JSON对象中添加键值对,可以使用put函数,见代码第③行和第⑧行。
代码第⑪行和第⑫行的toJsonString 函数转换为字符串,真正完成了JSON编码过程,prettyPrint
= true可以输出经过格式化的字符串。
2.解码
解码过程是编码反向操作,如果有如下JSON字符串:
{“name”:“tony”, “age”:30,
“a”:[1, 3]}
那么如何把这个JSON字符串解码成JSON对象或数组,参考代码如下:
val jsonString ="""{“name”:“tony”, “age”:30,
“a”:[1, 3]}""" ①
val parser = Parser() ②
val jsonObj = parser.parse(StringBuilder(jsonString)) as
JsonObject ③
val name = jsonObj.string(“name”) ④
println(“name : $name”)
val age = jsonObj.int(“age”) ⑤
println(“age : $age”)
val jsonAry = jsonObj.array(“a”) as
JsonArray
val n1 = jsonAry[0]
println(“数组a第一个元素 : $n1”)
val n2 = jsonAry[1]
println(“数组a第二个元素 : $n2”)
上述代码第①行是声明一个JSON字符串,网络通信过程中JSON字符串是从服务器返回的。代码第②行通过JSON字符串创建JSON对象,这个过程事实上就是JSON字符串解析过程,如果能够成功地创建JSON对象,说明解析成功,如果发生异常则说明解析失败。
代码第③行从JSON对象中按照名称取出JSON中对应的数据。代码第④行是取出一个JSON数组对象,代码第⑤行取出JSON数组第一个元素。
{ResultCode:0,Record:[
{ID:‘1’,CDate:‘2012-12-23’,Content:‘发布iOSBook0’,UserID:‘tony’},
{ID:‘2’,CDate:‘2012-12-24’,Content:‘发布iOSBook1’,UserID:‘tony’}]}
Kotlin可以通过使用Java的java.net.URL类进行高层次网络编程类,通过URL类访问互联网资源。使用URL进行网络编程,不需要对协议本身有太多的了解,相对而言是比较简单的。
23.5.1 URL概念
互联网资源是通过URL指定的,URL是Uniform Resource Locator简称,翻译过来是“一致资源定位器”,但人们都习惯URL简称。
URL组成格式如下:
协议名://资源名
“协议名”指明获取资源所使用的传输协议,如http、ftp、gopher和file等,“资源名”则应该是资源的完整地址,包括主机名、端口号、文件名或文件内部的一个引用。例如:
http://www.sina.com/
http://home.sohu.com/home/welcome.html
http://www.51work6.com:8800/Gamelan/network.html#BOTTOM
23.5.2 HTTP/HTTPS协议
访问互联网大多都基于HTTP/HTTPS协议。下面介绍一下HTTP/HTTPS协议。
1.HTTP协议
HTTP是Hypertext Transfer Protocol的缩写,即超文本传输协议。HTTP是一个属于应用层的面向对象的协议,其简捷、快速的方式适用于分布式超文本信息的传输。它于1990年提出,经过多年的使用与发展,得到不断完善和扩展。HTTP协议支持C/S网络结构,是无连接协议,即每一次请求时建立连接,服务器处理完客户端的请求后,应答给客户端然后断开连接,不会一直占用网络资源。
HTTP/1.1协议共定义了8种请求函数:OPTIONS、HEAD、GET、POST、PUT、DELETE、TRACE和CONNECT。在HTTP访问中,一般使用GET和HEAD函数。
GET函数。是向指定的资源发出请求,发送的信息“显式”地跟在URL后面。GET函数应该只用在读取数据,例如静态图片等。GET函数有点像使用明信片给别人写信,“信内容”写在外面,接触到的人都可以看到,因此是不安全的。
POST函数。是向指定资源提交数据,请求服务器进行处理,例如提交表单或者上传文件等。数据被包含在请求体中。POST函数像是把“信内容”装入信封中,接触到的人都看不到,因此是安全的。
2.HTTPS协议
HTTPS是Hypertext Transfer Protocol Secure,即超文本传输安全协议,是超文本传输协议和SSL的组合,用以提供加密通信及对网络服务器身份的鉴定。
简单地说,HTTPS是HTTP的升级版,HTTPS与HTTP的区别是:HTTPS使用https://代替http://,HTTPS使用端口443,而HTTP使用端口80来与TCP/IP进行通信。SSL使用40位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。HTTPS和SSL支持使用X.509数字认证,如果需要的话,用户可以确认发送者是谁。
23.5.3 使用URL类
java.net.URL类用于请求互联网上的资源,采用HTTP/HTTPS协议,请求函数是GET函数,一般是请求静态的、少量的服务器端数据。
URL类常用构造函数:
o URL(spec: String!)。根据字符串表示形式创建URL对象。
o URL(protocol: String!, host: String!, file: String!)。根据指定的协议名、主机名和文件名称创建URL对象。
o URL(protocol: String!, host: String!, port: Int!, file: String!)。根据指定的协议名、主机名、端口号和文件名称创建URL对象。
URL类常用函数:
o openStream()。打开到此URL的连接,并返回一个输入流InputStream对象。
o openConnection()。打开到此URL的新连接,返回一个URLConnection对象。
下面通过一个示例介绍一下如何使用java.net.URL类,示例代码如下:
//代码文件:chapter23/src/main/kotlin/com/a51work6/section5/ch23.5.3.kt
package com.a51work6.section5
import java.net.URL
fun main(args: Array) {
// Web网址
val url =“http://www.sina.com.cn/”
URL(url).openStream().use({input -> ①
input.bufferedReader().forEachLine { println(it) } ②
})
}
上述代码第①行URL(url)创建URL对象,参数是一个HTTP网址。然后调用URL对象的openStream()函数打开输入流。代码第②行input.bufferedReader()打开一个缓存区字符输入流BufferedReader,forEachLine函数是遍历输中数据。
23.5.4 使用HttpURLConnection发送GET请求
由于URL类只能发送HTTP/HTTPS的GET函数请求,如果要想发送其他的情况或者对网络请求有更深入的控制时,可以使用HttpURLConnection类型。
示例代码如下:
//代码文件:chapter23/src/main/kotlin/com/a51work6/section5/ch23.5.4.kt
package com.a51work6.section5
import java.net.HttpURLConnection
import java.net.URL
// Web服务网址
private val urlString =
“http://www.51work6.com/service/mynotes/WebService.php?” +
“email=<换成你在51work6.com注册时填写的邮箱>&type=JSON&action=query” ①
fun main(args: Array) {
var conn:HttpURLConnection? = null
try {
conn =URL(urlString).openConnection() as HttpURLConnection ②
conn.connect() ③
conn.inputStream.use { input -> ④
val data = input.bufferedReader().readText() ⑤
println(data)
}
} catch (e:Exception) {
e.printStackTrace()
} finally {
conn?.disconnect() ⑥
}
}
上述代码第①行是一个Web服务网址字符串。
代码第②行是创建URL对象并打开一个网络连接,URL(urlString)是创建URL对象,openConnection()函数是打开一个连接,返回URLConnection对象,由于本次连接是HTTP连接,所以返回的是HttpURLConnection对象。URLConnection是抽象类,HttpURLConnection是URLConnection的子类。代码第③行conn.connect()是建立网络连接。
代码第④行是通过conn.inputStream属性(或getInputStream()函数)打开输入流,上一节实例使用的URL的openStream()函数获得输入流。代码第⑤行读取流中的字符串。
代码第⑥行conn?.disconnect()是断开连接,这可以释放资源。
从服务器端返回的数据是JSON字符串,格式化后内容如下:
{
“ResultCode”: 0,
“Record”: [
{
“ID”: 5238,
“CDate”: “2017-05-18”,
“Content”: “欢迎来到智捷课堂。”
},
{
“ID”: 5239,
“CDate”: “2018-10-18”,
“Content”: “Welcome to zhijieketang.”
}
]
}
23.5.5 使用HttpURLConnection发送POST请求
HttpURLConnection也可以发送HTTP/HTTPS的POST请求,下面介绍如何使用HttpURLConnection发送POST请求。
示例代码如下:
//代码文件:chapter23/src/main/kotlin/com/a51work6/section5/ch23.5.5.kt
package com.a51work6.section5
import java.io.DataOutputStream
import java.net.HttpURLConnection
import java.net.URL
private val urlString =
“http://www.51work6.com/service/mynotes/WebService.php” ①
fun main(args: Array) {
var conn: HttpURLConnection? = null
try {
conn =URL(urlString).openConnection() as HttpURLConnection
conn.requestMethod =“POST” //POST请求 ②
conn.doOutput = true ③
// POST请求参数
val param =String.format("email=%s&type=%s&action=%s",
"[email protected]",
“JSON”, “query”) ④
// 设置参数
DataOutputStream(conn.outputStream).use { dStream -> ⑤
dStream.writeBytes(param) ⑥
}
conn.connect()
conn.inputStream.use { input->
val data = input.bufferedReader().readText()
println(data)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
conn?.disconnect()
}
}
上述代码第①行URL后面不带参数,这是因为要发送的是POST请求,POST请求参数是放在请求体中。代码第②行是设置HTTP请求函数为POST,代码第③行conn.doOutput =
true是设置请求过程可以传递参数给服务器。
代码第④行设置请求参数格式化字符串"email=%s&type=%s&action=%s",其中%s是占位符。
代码第⑤行~第⑥行是将请求参数发送给服务器,代码第⑤行是创建数据输出流DataOutputStream对象。代码第⑥行dStream.writeBytes(param)是向输出流中写入数据。
23.5.6 实例:Downloader
为了进一步熟悉URL类,这一节介绍一个下载程序Downloader,代码如下:
//代码文件:chapter23/src/main/kotlin/com/a51work6/section5/ch23.5.6.kt
package com.a51work6.section5
import java.io.BufferedOutputStream
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
// Web服务网址
private val urlString = “https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png”
fun main(args: Array) {
var conn: HttpURLConnection? = null
try {
conn =URL(urlString).openConnection() as HttpURLConnection
conn.connect()
conn.inputStream.use { input-> ①
BufferedOutputStream(FileOutputStream("./download.png")).use {
output -> ②
input.copyTo(output) ③
}
}
println(“下载成功”)
} catch (e: Exception) {
println(“下载失败”)
} finally {
conn?.disconnect()
}
}
上述代码第①行通过连接对象获得输入流input对象,代码第②行是FileOutputStream("./download.png")是创建文件输出流,然后又创建缓冲流输出流。代码第③行实现从输出流到输入流复制,由于输出流文件输出流,因此实现下载。注意下载成功后会在当前项目目录下生成一个download.png文件。
本章主要介绍了Kotlin网络编程,首先介绍了一些网络方面的基本知识。然后重点介绍了TCP Socket编程和UDP Socket编程。接着介绍了数据交换格式,重点介绍了JSON数据交换格式,由于Kotlin官方没有提供JSON解码和编码库,需要是使用第三方库。最后介绍了使用URL类访问互联网资源。