仓颉之网络编程 2024-09-04 周三

网络编程概述

  • 仓颉为开发者提供了基础的网络编程功能,在仓颉标准库中,用户可使用 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 类型套接字举例,仓颉在可靠传输场景下的可参考的编程模型如下:

  1. 创建服务端套接字,并指定本端绑定地址。
  2. 执行绑定。
  3. 执行 accept 动作,将阻塞等待,直到获取到一个客户端套接字连接。
  4. 同步创建客户端套接字,并指定远端的待连接的地址。
  5. 执行连接。
  6. 连接成功后,服务端会在 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 类型套接字举例,仓颉在不可靠传输场景下的可参考的编程模型如下:

  1. 创建套接字,并指定本端绑定地址。
  2. 执行绑定。
  3. 指定远端地址进行报文发送。
  4. 不连接远端地址场景下,可以收取来自不同远端地址的报文,并返回远端地址信息。
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]
*/
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容