版权声明:本文为作者原创书籍。转载请注明作者和出处,未经授权,严禁私自转载,侵权必究!!!
情感语录: 人生的游戏不在于拿了一副好牌,而在于怎样去打好坏牌,世上没有常胜将军,勇于超越自我者才能得到最后的奖杯。
欢迎来到本章节,上一章节介绍了Flutter 中的 Isolate
的使用,知识点回顾 戳这里 Flutter第十七章
本章节来介绍下 Flutter 的中 WebSocket,说起 WebSocket 那就不得不说 Dart 中的 Socket了,因为 Flutter 中的 WebSocket 非常类似 Socket 。但它不同与 Socket的是它在进行协议升级之前是使用的 HTTP 协议进行连接握手,而在升级后则使用的 TCP协议进行传输 ,即可以实现持久连接的全双工双向通信。 那什么是 Socket 呢? 它就是我们常说的 套接字
,用于数据传输的长链接。你可能会说数据传输完全可以使用 HTTP 协议呀。是这样的,HTTP是可以做数据传输,但 HTTP协议是无状态的,只能由客户端主动发起,服务端再被动响应,服务端无法主动向客户端发送消息,一旦服务端响应结束,链接就会断开,即 一次请求一次响应,请求之间没有持续的连接 ,因此无法进行实时通信。那么 Socket 使用的是什么协议呢? 请客官继续往下看喏 O(∩_∩)O
一、初识套接字(Socket)
Dart 中的 套接字可以大致分为三类:流格式套接字(SOCK_STREAM)、数据报套接字(SOCK_DGRAM)、原始套接字(SOCK_RAW)。
1.1、流格式套接字(Socket、ServerSocket),它使用的是TCP协议,是面向有连接的协议、既能保证可靠的数据传输,实现数据的无差错、无重复发送,并按顺序接收,TCP 强调的是传输的可靠性,在传输速度上相对较慢。
1.2、数据报套接字(RawDatagramSocket),它使用的是UDP协议,是一种面向无连接的协议,不能保证数据传输的可靠性,且无法保证顺序。UDP强调的是快速传输而非去确保传输的可靠性。
1.3、原始套接字(RawSocket、RawServerSocket),允许对较低层次的协议进行访问 比如IP、ICMP、IGMP协议,可以用来接收TCP/IP栈不能处理的IP包 或发送自定义包头、自定义协议的IP包。
你可能会觉得 这就是Dart 中的全部套接字了。事实并非如此,出于安全方面考虑,在Dart 中还给出了使用TLS和SSL协议的安全套接字,看下面内部源码片段:
/**
* A high-level class for communicating securely over a TCP socket, using
* TLS and SSL. The [SecureSocket] exposes both a [Stream] and an
* [IOSink] interface, making it ideal for using together with
* other [Stream]s.
*/
abstract class SecureSocket implements Socket {
external factory SecureSocket._(RawSecureSocket rawSocket);
// ....省略
}
/**
* The [SecureServerSocket] is a server socket, providing a stream of high-level
* [Socket]s.
*
* See [SecureSocket] for more info.
*/
class SecureServerSocket extends Stream<SecureSocket> {
// ....省略
}
/**
* RawSecureSocket provides a secure (SSL or TLS) network connection.
* Client connections to a server are provided by calling
* RawSecureSocket.connect. A secure server, created with
* [RawSecureServerSocket], also returns RawSecureSocket objects representing
* the server end of a secure connection.
* The certificate provided by the server is checked
* using the trusted certificates set in the SecurityContext object.
* The default [SecurityContext] object contains a built-in set of trusted
* root certificates for well-known certificate authorities.
*/
abstract class RawSecureSocket implements RawSocket {
// ....省略
}
/**
* The RawSecureServerSocket is a server socket, providing a stream of low-level
* [RawSecureSocket]s.
*
* See [RawSecureSocket] for more info.
*/
class RawSecureServerSocket extends Stream<RawSecureSocket> {
// ....省略
}
不同的套接字对应着不同的协议,对于套接字的选用往往需要根据你的业务场景来选取,如你需要做文件传输,网络直播等需求时则需要传输速率较快的 UDP协议,而如你需要做 类似聊天消息,邮件等确保每个内容都能送达的业务时则需要可靠的 TCP协议。那么为什么 TCP 就可靠,UDP就不可靠呢? 这里涉及到连接时的多次握手和多次挥手机制和相关原理,这本应该是 Dart 中的内容,但由于 Dart 章节只是针对 Flutter 快速入门做的介绍,因此并未深入,关于协议方面的内容,将会在后期的 Java 专栏做深入剖析,因为一篇内容根本解析不完(如果有机会的话O(∩_∩)O),这里只需记住上面的几个关键点即可,持续叨逼叨..........
二、Socket 的使用
2.1 流格式套接字
流格式套接字分为了客户端(Socket) 和服务端(ServerSocket),客户端和服务端要能正常的通信则需要服务端提前告诉 客户端 IP地址和端口号,而客户端则使用 该IP地址和端口号进行链接。那什么是 IP地址和端口号呢? 举个例子吧! 你可以把 提供服务的 ServerSocket 看做是我们的插座,而 Socket 则是我们需要充电的插头设备,Socket 想要充电首先得找到我们的插座在哪里(即 IP地址),而 ServerSocket 只提供了 三空的插座口,因此你的插头设备 Socket 也必须的要三空插口(即 port 端口号)才能进行匹配充电, 只有这样无缝深深的插入后才能做更亲密的交流.............
首先来看下 客户端(Socket)
abstract class Socket implements Stream<Uint8List>, IOSink {
/**
* Creates a new socket connection to the host and port and returns a [Future]
* that will complete with either a [Socket] once connected or an error
* if the host-lookup or connection failed.
*
* [host] can either be a [String] or an [InternetAddress]. If [host] is a
* [String], [connect] will perform a [InternetAddress.lookup] and try
* all returned [InternetAddress]es, until connected. Unless a
* connection was established, the error from the first failing connection is
* returned.
*
* The argument [sourceAddress] can be used to specify the local
* address to bind when making the connection. `sourceAddress` can either
* be a `String` or an `InternetAddress`. If a `String` is passed it must
* hold a numeric IP address.
*
* The argument [timeout] is used to specify the maximum allowed time to wait
* for a connection to be established. If [timeout] is longer than the system
* level timeout duration, a timeout may occur sooner than specified in
* [timeout]. On timeout, a [SocketException] is thrown and all ongoing
* connection attempts to [host] are cancelled.
*/
static Future<Socket> connect(host, int port,
{sourceAddress, Duration timeout}) {
final IOOverrides overrides = IOOverrides.current;
if (overrides == null) {
return Socket._connect(host, port,
sourceAddress: sourceAddress, timeout: timeout);
}
return overrides.socketConnect(host, port,
sourceAddress: sourceAddress, timeout: timeout);
}
/// Like [connect], but returns a [Future] that completes with a
/// [ConnectionTask] that can be cancelled if the [Socket] is no
/// longer needed.
static Future<ConnectionTask<Socket>> startConnect(host, int port,
{sourceAddress}) {
final IOOverrides overrides = IOOverrides.current;
if (overrides == null) {
return Socket._startConnect(host, port, sourceAddress: sourceAddress);
}
return overrides.socketStartConnect(host, port,
sourceAddress: sourceAddress);
}
// ...... 省略
}
从源码中看到 Socket 是一个抽象类,因此不能直接通过 new
方式来创建Socket,只能通过 connect
和 startConnect
方法建立一个异步连接后拿到 Socket 对象。这两个方法都能和服务端建立连接, 那他们有什么区别呢?connect 方式建立的异步的连接后,可以直接拿到 Socket 对象,而使用 startConnect 方式则是先建立了一个异步的连接任务,在连接成功后通过 ConnectionTask 对象才能获取到 Socket,但使用 startConnect 的方式,可以进行连接任务的取消(cancell),在使用上其实并没多大区别。
参数介绍:
①、host(必传):
指定需要连接的主机地址,服务端提供,如:“192.168.xx.xx” 。
②、port (必传):
:指定服务的端口号,服务端提供。
③、sourceAddress:可用于指定在建立连接时要绑定的本地地址。
④、timeout:连接响应的最大超时时间。
服务端( ServerSocket )
abstract class ServerSocket implements Stream<Socket> {
/**
* Returns a future for a [:ServerSocket:]. When the future
* completes the server socket is bound to the given [address] and
* [port] and has started listening on it.
*
* The [address] can either be a [String] or an
* [InternetAddress]. If [address] is a [String], [bind] will
* perform a [InternetAddress.lookup] and use the first value in the
* list. To listen on the loopback adapter, which will allow only
* incoming connections from the local host, use the value
* [InternetAddress.loopbackIPv4] or
* [InternetAddress.loopbackIPv6]. To allow for incoming
* connection from the network use either one of the values
* [InternetAddress.anyIPv4] or [InternetAddress.anyIPv6] to
* bind to all interfaces or the IP address of a specific interface.
*
* If an IP version 6 (IPv6) address is used, both IP version 6
* (IPv6) and version 4 (IPv4) connections will be accepted. To
* restrict this to version 6 (IPv6) only, use [v6Only] to set
* version 6 only.
*
* If [port] has the value [:0:] an ephemeral port will be chosen by
* the system. The actual port used can be retrieved using the
* [port] getter.
*
* The optional argument [backlog] can be used to specify the listen
* backlog for the underlying OS listen setup. If [backlog] has the
* value of [:0:] (the default) a reasonable value will be chosen by
* the system.
*
* The optional argument [shared] specifies whether additional ServerSocket
* objects can bind to the same combination of `address`, `port` and `v6Only`.
* If `shared` is `true` and more `ServerSocket`s from this isolate or other
* isolates are bound to the port, then the incoming connections will be
* distributed among all the bound `ServerSocket`s. Connections can be
* distributed over multiple isolates this way.
*/
external static Future<ServerSocket> bind(address, int port,
{int backlog: 0, bool v6Only: false, bool shared: false});
参数介绍:
①、address (必传) :
指定监听的地址,如:“192.168.xx.xx” 。
②、port (必传)
:指定监听的端口号。
③、backlog :一般不传,默认为0.
④、v6Only:是否启用 IPV6.
⑤、shared:是否在新来的连接中分布到其他隔离器中去。
可见客户端和服务端都不能直接通过 new 来创建对象,这和Java 中的 Socket 区别还是蛮大的,ServerSocket 则是在 通过 bind
后再能取得该对象。
下面使用 Socket,和 ServerSocket来做个极其简易的聊天对话。
客户端:
import 'dart:convert';
import 'dart:io';
void main(){
//创建一个Socket连接到指定地址与端口
Socket.connect(InternetAddress.loopbackIPv4, 8000).then((socket) {
//向服务端发送数据
socket.write('你好啊 我是客户端');
print(socket.address);
//向服务端发送数据
socket.add(utf8.encode("客户端通过 add 方式发生的数据"));
//监听服务端的发送过来数据
socket.listen((data) {
print("接收到来自Server的数据:${utf8.decode(data)}");
});
});
}
服务端:
import 'dart:convert';
import 'dart:io';
void main() {
//绑定地址和端口,获取套接字,监听每个连接
ServerSocket.bind(InternetAddress.loopbackIPv4, 8000).then((serverSocket) {
//监听客户端发送过来的数据
serverSocket.listen((socket) {
//得到客户端的 socket,取出数据
socket.listen((data) {
print("接收到来自Client的数据: ${utf8.decode(data)}");
//向客户端会送一条数据
var content = "收到你的数据 ${utf8.decode(data)}";
//向客户端推送数据
socket.write(content);
});
});
});
}
首先将服务端 ServerSocket 启动起来,然后再启动客户端。接下来看效果:
服务端输出结果:
接收到来自Client的数据: 你好啊 我是客户端
接收到来自Client的数据: 客户端通过 add 方式发生的数据
客户端输出结果:
InternetAddress('127.0.0.1', IPv4)
接收到来自Server的数据:收到你的数据 你好啊 我是客户端
接收到来自Server的数据:收到你的数据 客户端通过 add 方式发生的数据
2.2 数据报套接字
数据报套接字(RawDatagramSocket)并没有区分客户端和服务端,客户端和服务端都是使用的 RawDatagramSocket 既任何一方都可以作为是 服务端。
/**
* Creates a new raw datagram socket binding it to an address and
* port.
*/
external static Future<RawDatagramSocket> bind(host, int port,
{bool reuseAddress: true, bool reusePort: false, int ttl: 1});
参数介绍:
①、host (必传) :
指定链接的地址 。
②、port (必传):
指定监听的端口号。
③、reuseAddress: 是否允许多个进程同时监听、绑定同一个端口。
④、reusePort: 是否复用端口。
⑤、ttl:指生存时间,默认为1 ,一般不用设置。TTL经过一个路由器后就减一,当TTL为0时,则将数据包丢弃,防止两个路由器之间可能形成死循环。
将上面的简易聊天改成 RawDatagramSocket 方式。
客户端:
import 'dart:io';
import 'dart:convert';
main() {
//创建一个数据包 并绑定地址和端口
var rawDgramSocket = RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 8002);
rawDgramSocket.then((socket) {
socket.send(utf8.encode("你好服务端!"), InternetAddress.loopbackIPv4, 8001);
socket.listen((event) {
if(event == RawSocketEvent.read) {
//打印服务端的数据
print("来自服务端的数据 ${utf8.decode(socket.receive().data)}");
}
});
});
}
服务端:
import 'dart:io';
import 'dart:convert';
main() {
//创建一个数据包 并绑定地址和端口
var rawDgramSocket = RawDatagramSocket.bind(
InternetAddress.loopbackIPv4, 8001);
rawDgramSocket.then((socket) {
//监听套接字事件
socket.listen((event) {
if (event == RawSocketEvent.read) {
//通过事件 socket.receive 获取数据
print(utf8.decode(socket.receive().data));
socket.send(utf8.encode("已收到!"), InternetAddress.loopbackIPv4, 8002);
}
});
});
}
这里就不贴结果了,运用后,服务端收到 客户端发来的 "你好服务端!", 而客户端会打印出来自服务端的 “已收到!”
上面就是开发中常用的 Socket了,关于Dart中的其他 Socket 本章就不做介绍了,因为文章都很长了还没引出本章的重点知识。
三、Flutter 中的 WebSocket
开篇说了 WebSocket 既使用了应用层的 HTTP协议,又使用了传输层的 TCP 协议,简单点说 WebSocket也是一种协议,与HTTP协议一样位于应用层,都是TCP/IP协议的子集。你可能会问为什么不直接使用 HTTP协议呢? 你要得要服务端的最新数据,你循环不断的去请求服务端呗! 没错,是可以通过这种方式不断的去拉取,服务端不需要主动推送,但这样就会出现一个问题,在用户量和数据传输不大的情况下可以这么使用,一旦用户量上来或者数据传输变大,首先对流量的消耗变大不说,还会直接导致服务器性能下降,或挂掉。那为什么不直接使用TCP 呢? 其目的是为了在浏览器上得到兼容,因此才设计出这么一个全新的协议。
说了这么多,下面介绍下它的使用吧.....
3.1 WebSocket 插件引入
1、在pubspec.yaml
文件中添加依赖
dependencies:
fluttertoast: ^3.0.3
flutter:
sdk: flutter
#添加websocket 插件
web_socket_channel: ^1.1.0
本人使用的当前最新版本 1.1.0
,读者想体验最新版本请在使用时参看最新版本号进行替换。
2、安装依赖库
执行 flutter packages get
命令;AS 开发工具直接右上角 packages get
也可。
3、在需要使用的地方导包引入
import 'package:web_socket_channel/io.dart'
3.2 WebSocket 使用和解读
该插件给我们提供了两种常用的 WebSocket。一个 是直接在代码中用的IOWebSocketChannel
,一个则是主要在 网页上用的 HtmlWebSocketChannel
。两者在使用上基本一样。下面介绍 IOWebSocketChannel 即可。
还是先从源码中说起吧,进入 IOWebSocketChannel内部源码:
/// A [WebSocketChannel] that communicates using a `dart:io` [WebSocket].
class IOWebSocketChannel extends StreamChannelMixin
implements WebSocketChannel {
/// The underlying `dart:io` [WebSocket].
///
/// If the channel was constructed with [IOWebSocketChannel.connect], this is
/// `null` until the [WebSocket.connect] future completes.
WebSocket _webSocket;
String get protocol => _webSocket?.protocol;
int get closeCode => _webSocket?.closeCode;
String get closeReason => _webSocket?.closeReason;
final Stream stream;
final WebSocketSink sink;
// TODO(nweiz): Add a compression parameter after the initial release.
/// Creates a new WebSocket connection.
///
/// Connects to [url] using [WebSocket.connect] and returns a channel that can
/// be used to communicate over the resulting socket. The [url] may be either
/// a [String] or a [Uri]. The [protocols] and [headers] parameters are the
/// same as [WebSocket.connect].
///
/// [pingInterval] controls the interval for sending ping signals. If a ping
/// message is not answered by a pong message from the peer, the WebSocket is
/// assumed disconnected and the connection is closed with a `goingAway` code.
/// When a ping signal is sent, the pong message must be received within
/// [pingInterval]. It defaults to `null`, indicating that ping messages are
/// disabled.
///
/// If there's an error connecting, the channel's stream emits a
/// [WebSocketChannelException] wrapping that error and then closes.
factory IOWebSocketChannel.connect(url,
{Iterable<String> protocols,
Map<String, dynamic> headers,
Duration pingInterval}) {
var channel;
var sinkCompleter = WebSocketSinkCompleter();
var stream = StreamCompleter.fromFuture(WebSocket.connect(url.toString(),
headers: headers, protocols: protocols)
.then((webSocket) {
webSocket.pingInterval = pingInterval;
channel._webSocket = webSocket;
sinkCompleter.setDestinationSink(_IOWebSocketSink(webSocket));
return webSocket;
}).catchError((error) => throw WebSocketChannelException.from(error)));
channel = IOWebSocketChannel._withoutSocket(stream, sinkCompleter.sink);
return channel;
}
/// Creates a channel wrapping [socket].
IOWebSocketChannel(WebSocket socket)
: _webSocket = socket,
stream = socket.handleError(
(error) => throw WebSocketChannelException.from(error)),
sink = _IOWebSocketSink(socket);
/// Creates a channel without a socket.
///
/// This is used with [connect] to synchronously provide a channel that later
/// has a socket added.
IOWebSocketChannel._withoutSocket(Stream stream, this.sink)
: _webSocket = null,
stream = stream.handleError(
(error) => throw WebSocketChannelException.from(error));
}
可以看到 IOWebSocketChannel 继承了 StreamChannelMixin 类并实现了 WebSocketChannel 类(它并不是一个接口),HtmlWebSocketChannel内部也是如此。在内部有三个构造函数,其中 _withoutSocket
为外部不能使用的私有构造不做介绍。命名构造函数 connect
则是我们常用的,而默认的一半不用,因为它必须的传入一个 WebSocket,而 WebSocket 又是一个抽象类,你还得去实现这个抽象类。故此一般情况不会使用它。
connect 方法的参数介绍:
①、url (必传) :
指定链接的主机地址,它可以是一个 String类型的地址,也可以是一个 Uri对象。
②、protocols(非必传) :
附加的一些协议信息。
③、headers(非必传) :
附加的一些头信息。
④、pingInterval(非必传) :
控制发送ping信号的间隔时间,在指定时间内没收到 服务端的 pong 信息会送,则会断开链接。
接下来使用由websocket.org提供的回声测试服务器进行代码演练下,将输入的内容发送给服务器,然后在原样返回回来。
import 'package:flutter/material.dart';
import 'package:web_socket_channel/io.dart';
class WebSocketPage extends StatefulWidget {
@override
_WebSocketPageState createState() => _WebSocketPageState();
}
class _WebSocketPageState extends State<WebSocketPage> {
TextEditingController sendController = new TextEditingController();
//声明一个句柄
IOWebSocketChannel channel;
var sendContent = "";
@override
void initState() {
super.initState();
//添加监听
sendController.addListener(() {
sendContent = sendController.text;
});
//创建对象 并建立连接
channel = IOWebSocketChannel.connect('ws://echo.websocket.org');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("WebSocketPage"),
),
body: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Column(
children: <Widget>[
Form(
child: Row(
children: <Widget>[
SizedBox(
width: 200,
child: TextFormField(
autofocus: false,
controller: sendController,
decoration: InputDecoration(
labelText: "发送者",
hintText: "请输入要发送的内容",
),
)),
SizedBox(width: 10),
RaisedButton(
child: Text("发送"),
onPressed: () {
if (sendContent.isNotEmpty) {
// 将要发送的数据添加到数据池中
channel.sink.add(sendContent);
}
},
)
],
),
),
SizedBox(height: 30),
Text("来自服务端的信息"),
StreamBuilder(
stream: channel.stream,
builder: (context, snapshot) {
var backContent = "";
//网络错误
if (snapshot.hasError) {
backContent = "网络不通...";
} else if (snapshot.hasData) {
backContent = "收到内容回声: " + snapshot.data;
}
return Padding(
padding: EdgeInsets.symmetric(vertical: 24.0),
child: Text(backContent),
);
},
)
],
),
),
);
}
@override
void dispose() {
super.dispose();
//关闭通道
channel.sink.close();
}
}
温馨提示:
由于WebScoket使用的是一种新的协议,所以URL与HTTP协议略有不同。未加密的连接为ws://,而不是http://。加密的连接为wss://,而不是https://。
效果如下:
可见 WebSocket 在建立连接后确实能发能收消息了,那这个消息内部是怎么处理的呢?在源码中通过 channel.sink.add("xxxx")
将数据进行了外发,在 StreamBuilder
中能接收到数据回传。下面来分开介绍下吧:
3.3、发送数据
在发送数据时 IOWebSocketChannel 使用了自身内部的一个对象实例 sink(WebSocketSink),这个 WebSocketSink 究竟是个什么玩意儿呢?
WebSocketSink 其实就是被 DelegatingStreamSink
包装的一个 StreamSink
,而 StreamSink又是一个 同步和异步地接受流事件的对象,最终通过流的方式将数据写出。源码如下:
/// Simple delegating wrapper around a [StreamSink].
///
/// Subclasses can override individual methods, or use this to expose only the
/// [StreamSink] methods of a subclass.
class DelegatingStreamSink<T> implements StreamSink<T> {
final StreamSink _sink;
Future get done => _sink.done;
/// Create delegating sink forwarding calls to [sink].
DelegatingStreamSink(StreamSink<T> sink) : _sink = sink;
DelegatingStreamSink._(this._sink);
/// Creates a wrapper that coerces the type of [sink].
///
/// Unlike [new StreamSink], this only requires its argument to be an instance
/// of `StreamSink`, not `StreamSink<T>`. This means that calls to [add] may
/// throw a [CastError] if the argument type doesn't match the reified type of
/// [sink].
static StreamSink<T> typed<T>(StreamSink sink) =>
sink is StreamSink<T> ? sink : DelegatingStreamSink._(sink);
void add(T data) {
_sink.add(data);
}
void addError(error, [StackTrace stackTrace]) {
_sink.addError(error, stackTrace);
}
Future addStream(Stream<T> stream) => _sink.addStream(stream);
Future close() => _sink.close();
}
3.4、接收数据
StreamBuilder 为什么能接收到数据?它其实是一个 StatefulWidget 组件,只是内部绑定了一个 可以接收数据的 Stream, 即IOWebSocketChannel 中的实例 stream 。而 Stream是异步数据事件的源,因此在绑定 stream 的 StreamBuilder 中就能收到数据了。StreamBuilder 在接收到数据后会通知组件树进行异步刷新,等同于调用 了 setState()
方法。故此,在使用 StreamBuilder 时应该避免深层次的嵌套。在 Flutter 中能进行异步刷新的组件除了 StreamBuilder 外,还提供了 FutureBuilder
组件,它其实也是StatefulWidget的子类,但它接收的不是一个 stream ,而是一个耗时的 Future
,除此之外在使用上没什么多大区别,这里就不做介绍了。
不在 使用WebSocket后一定要及时的关闭连接
//关闭通信连接
channel.sink.close();
写在最后
HTTP 、WebSocket、Socket 三者有什么区别
① 、HTTP 就是简单的一问一答模式,它最大的缺陷就是通信只能由客户端发起,做不到服务器主动向客户端推送信息。
②、可以把WebSocket想象成HTTP(应用层),HTTP和Socket什么关系,WebSocket和Socket就是什么关系,不同的是Websocket解决了服务器与客户端全双工通信的问题。
③、Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
备注
:什么是单工、半双工、全双工通信?
单工: 信息只能单向传送为单工。
半双工:信息能双向传送但不能同时双向传送称为半双工。
全双工:信息能够同时双向传送则称为全双工。
好了本章节到此结束,又到了说再见的时候了,如果你喜欢请留下你的小红星;你们的支持才是创作的动力,如有错误,请热心的你留言指正, 谢谢大家观看,下章再会 O(∩_∩)O
实例源码地址:https://github.com/zhengzaihong/flutter_learn/blob/master/lib/page/websocket/WebSocketPage.dart