网络编程概述
仓颉为开发者提供了基础的网络编程功能,在仓颉标准库中,用户可使用 std 模块下的 socket 包来实现传输层网络通信。
在传输层协议中,分为不可靠传输和可靠传输两种,仓颉将其抽象为 DatagramSocket 和 StreamSocket。其中不可靠传输协议常见的是 UDP,可靠传输协议常见的是 TCP,仓颉分别将其抽象为 UdpSocket 和 TcpSocket。
而在应用层协议中,较为常见的是 HTTP 协议,常用于开发 Web 应用程序等。当前 HTTP 协议已有多个版本,仓颉目前支持 HTTP/1.1、HTTP/2.0 等。
WebSocket 作为一种提升 Web 服务端与客户端间的通信效率的应用层协议,仓颉将其抽象为 WebSocket 对象,并支持从 HTTP 协议升级至 WebSocket 协议。
需要注意的是,仓颉的网络编程是阻塞式的。但被阻塞的是仓颉线程,阻塞中的仓颉线程会将系统线程让渡出去,因此并不会真正阻塞一个系统线程。
Socket 编程
仓颉的 Socket 编程指的是基于传输层协议实现网络传输数据包的功能。
在可靠传输场景下,仓颉分别启动客户端套接字和服务端套接字。客户端套接字必须指定将要连接的远端地址,可选择性地绑定本端地址,在连接成功后,才可以收发报文。而服务端套接字必须绑定本端地址,在绑定成功后,才可以收发报文。
在不可靠传输场景下,套接字无需区分客户端和服务端,仓颉分别启动两个套接字进行数据传输。套接字必须绑定本端地址,绑定成功后,才可以收发报文。并且,套接字也可选择性地指定远端连接地址,指定后将仅接受指定的远端地址的报文,同时在 send 时无需指定远端地址,报文将发送至成功连接的地址。
Tcp 编程
Tcp 作为一种常见的可靠传输协议,以 Tcp 类型套接字举例,仓颉在可靠传输场景下的可参考的编程模型如下:
- 创建服务端套接字,并指定本端绑定地址。
- 执行绑定。
- 执行 accept 动作,将阻塞等待,直到获取到一个客户端套接字连接。
- 同步创建客户端套接字,并指定远端的待连接的地址。
- 执行连接。
- 连接成功后,服务端会在 accept 接口返回一个新的套接字,此时服务端可以通过此套接字进行读写操作,即收发报文。客户端则可以直接进行读写操作。
import std.socket.*
import std.time.*
import std.sync.*
let SERVER_PORT: UInt16 = 8080
func runTcpServer() {
try (serverSocket = TcpServerSocket(bindAt: SERVER_PORT)) {
serverSocket.bind()
try (client = serverSocket.accept()) {
let buf = Array<Byte>(10, item: 0)
let count = client.read(buf)
// 服务端读取到的数据为: [1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
println("Server read ${count} bytes: ${buf}")
}
}
}
main(): Int64 {
let future = spawn {
runTcpServer()
}
sleep(Duration.millisecond * 500)
try (socket = TcpSocket("127.0.0.1", SERVER_PORT)) {
socket.connect()
socket.write(Array<Byte>([1, 2, 3]))
}
future.get()
return 0
}
/*
Server read 3 bytes: [1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
*/
Udp 编程
Udp 作为一种常见的不可靠传输协议,以 Udp 类型套接字举例,仓颉在不可靠传输场景下的可参考的编程模型如下:
- 创建套接字,并指定本端绑定地址。
- 执行绑定。
- 指定远端地址进行报文发送。
- 不连接远端地址场景下,可以收取来自不同远端地址的报文,并返回远端地址信息。
import std.socket.*
import std.time.*
import std.sync.*
let SERVER_PORT: UInt16 = 8080
func runUpdServer() {
try (serverSocket = UdpSocket(bindAt: SERVER_PORT)) {
serverSocket.bind()
let buf = Array<Byte>(3, item: 0)
let (clientAddr, count) = serverSocket.receiveFrom(buf)
let sender = clientAddr.hostAddress
// Server receive 3 bytes: [1, 2, 3] from 127.0.0.1
println("Server receive ${count} bytes: ${buf} from ${sender}")
}
}
main(): Int64 {
let future = spawn {
runUpdServer()
}
sleep(Duration.second)
try (udpSocket = UdpSocket(bindAt: 0)) {
udpSocket.sendTimeout = Duration.second * 2
udpSocket.bind()
udpSocket.sendTo(
SocketAddress("127.0.0.1", SERVER_PORT),
Array<Byte>([1, 2, 3])
)
}
future.get()
return 0
}
/*
Server receive 3 bytes: [1, 2, 3] from 127.0.0.1
*/
HTTP 编程
HTTP 作为一种通用的应用层协议,通过请求-响应的机制实现数据传输,客户端发送请求,服务端返回响应。请求和响应的格式是固定的,由报文头和报文体组成。
常用的请求类型为 GET 和 POST,GET 请求只有报文头,用于向服务器请求应用层数据,POST 请求带有报文体,以一个空行与报文头进行分隔,用于向服务器提供应用层数据。
请求-响应的报文头字段内容较多,此处不再一一赘述,仓颉支持 HTTP 1.0/1.1/2.0 等协议版本,开发者可以基于协议 RFC 9110、9112、9113、9218、7541 以及仓颉所提供的 HttpRequestBuilder 和 HttpResponseBuilder 类构造请求及响应报文。
import net.http.*
import std.time.*
import std.sync.*
import std.log.LogLevel
func startServer(): Unit {
// 1. 构建 Server 实例
let server = ServerBuilder()
.addr("127.0.0.1")
.port(8080)
.build()
// 2. 注册请求处理逻辑
server.distributor.register("/hello", {httpContext =>
httpContext.responseBuilder.body("Hello Cangjie!")
})
server.logger.level = OFF
// 3. 启动服务
server.serve()
}
func startClient(): Unit {
// 1. 构建 client 实例
let client = ClientBuilder().build()
// 2. 发送 request
let response = client.get("http://127.0.0.1:8080/hello")
// 3. 读取response body
let buffer = Array<Byte>(32, item: 0)
let length = response.body.read(buffer)
println(String.fromUtf8(buffer[..length]))
// 4. 关闭连接
client.close()
}
main () {
spawn {
startServer()
}
sleep(Duration.second)
startClient()
}
/*
Hello Cangjie!
*/
WebSocket 编程
在网络编程中,WebSocket 也是一种常用的应用层协议,与 HTTP 一样,它也基于 TCP 协议之上,并且常用于 web 服务端应用开发。
不同于 HTTP 的是, WebSocket 只需要客户端和服务端进行一次握手,即可创建长久的连接,并且进行双向的数据传输。即,基于 WebSocket 实现的服务端可以主动传输数据给客户端,从而实现实时通讯。
WebSocket 是一个独立的协议,它与 HTTP 的关联在于,它的握手被 HTTP 服务端解释为一个升级请求。因此,仓颉将 WebSocket 包含在 http 包中。
仓颉将 WebSocket 协议通信机制抽象为 WebSocket 类,提供方法将一个 http/1.1 或 http/2.0 服务端句柄升级到 WebSocket 协议实例,通过返回的 WebSocket 实例进行 WebSocket 通信,例如数据报文的读写。
在仓颉中,WebSocket 所传输的数据基本单元称为帧,帧分为两类,一类为传输控制信息的帧,即 Close Frame 用于关闭连接, Ping Frame 用于实现 Keep-Alive , Pong Frame 是 Ping Frame 的响应类型,另一类是传输应用数据的帧,应用数据帧支持分段传输。
-仓颉的帧由三个属性构成,其中 fin 和 frameType 共同说明了帧是否分段和帧的类型,payload 为帧的载荷,除此之外开发者无需关心其他属性即可进行报文传输。
import net.http.*
import encoding.url.*
import std.time.*
import std.sync.*
import std.collection.*
import std.log.*
let server = ServerBuilder()
.addr("127.0.0.1")
.port(0)
.build()
// client:
main() {
// 1 启动服务器
spawn { startServer() }
sleep(Duration.millisecond * 200)
let client = ClientBuilder().build()
let u = URL.parse("ws://127.0.0.1:${server.port}/webSocket")
let subProtocol = ArrayList<String>(["foo1", "bar1"])
let headers = HttpHeaders()
headers.add("test", "echo")
// 2 完成 WebSocket 握手,获取 WebSocket 实例
let websocket: WebSocket
let respHeaders: HttpHeaders
(websocket, respHeaders) = WebSocket.upgradeFromClient(client, u, subProtocols: subProtocol, headers: headers)
client.close()
println("subProtocol: ${websocket.subProtocol}") // fool1
println(respHeaders.getFirst("rsp") ?? "") // echo
// 3 消息收发
// 发送 hello
websocket.write(TextWebFrame, "hello".toArray())
// 收
let data = ArrayList<UInt8>()
var frame = websocket.read()
while(true) {
match(frame.frameType) {
case ContinuationWebFrame =>
data.appendAll(frame.payload)
if (frame.fin) {
break
}
case TextWebFrame | BinaryWebFrame =>
if (!data.isEmpty()) {
throw Exception("invalid frame")
}
data.appendAll(frame.payload)
if (frame.fin) {
break
}
case CloseWebFrame =>
websocket.write(CloseWebFrame, frame.payload)
break
case PingWebFrame =>
websocket.writePongFrame(frame.payload)
case _ => ()
}
frame = websocket.read()
}
println("data size: ${data.size}") // 4097
println("last item: ${String.fromUtf8(Array(data)[4096])}") // a
// 4 关闭 websocket,
// 收发 CloseFrame
websocket.writeCloseFrame(status: 1000)
let websocketFrame = websocket.read()
println("close frame type: ${websocketFrame.frameType}") // CloseWebFrame
println("close frame payload: ${websocketFrame.payload}") // 3, 232
// 关闭底层连接
websocket.closeConn()
server.close()
}
func startServer() {
// 1 注册 handler
server.distributor.register("/webSocket", handler1)
server.logger.level = OFF
server.serve()
}
// server:
func handler1(ctx: HttpContext): Unit {
// 2 完成 websocket 握手,获取 websocket 实例
let websocketServer = WebSocket.upgradeFromServer(ctx, subProtocols: ArrayList<String>(["foo", "bar", "foo1"]),
userFunc: {request: HttpRequest =>
let value = request.headers.getFirst("test") ?? ""
let headers = HttpHeaders()
headers.add("rsp", value)
headers
})
// 3 消息收发
// 收 hello
let data = ArrayList<UInt8>()
var frame = websocketServer.read()
while(true) {
match(frame.frameType) {
case ContinuationWebFrame =>
data.appendAll(frame.payload)
if (frame.fin) {
break
}
case TextWebFrame | BinaryWebFrame =>
if (!data.isEmpty()) {
throw Exception("invalid frame")
}
data.appendAll(frame.payload)
if (frame.fin) {
break
}
case CloseWebFrame =>
websocketServer.write(CloseWebFrame, frame.payload)
break
case PingWebFrame =>
websocketServer.writePongFrame(frame.payload)
case _ => ()
}
frame = websocketServer.read()
}
println("data: ${String.fromUtf8(Array(data))}") // hello
// 发 4097 个 a
websocketServer.write(TextWebFrame, Array<UInt8>(4097, item: 97))
// 4 关闭 websocket,
// 收发 CloseFrame
let websocketFrame = websocketServer.read()
println("close frame type: ${websocketFrame.frameType}") // CloseWebFrame
println("close frame payload: ${websocketFrame.payload}") // 3, 232
websocketServer.write(CloseWebFrame, websocketFrame.payload)
// 关闭底层连接
websocketServer.closeConn()
}
/*
subProtocol: foo1
echo
data: hello
data size: 4097
last item: a
close frame type: CloseWebFrame
close frame payload: [3, 232]
close frame type: CloseWebFrame
close frame payload: [3, 232]
*/