转载自 http://www.52im.net/thread-1722-1-1.html

1、引言

好多小白初次接触即时通讯(比如:IM或者消息推送应用)时,总是不能理解Web短连接(就是最常见的HTTP通信了)跟长连接(主要指TCP、UDP协议实现的socket通信,当然HTML5里的Websocket协议也是长连接)的区别,导致写即时通讯这类系统代码时往往找不到最佳实践,搞的一脸蒙逼。

本篇我们先简单了解一下 TCP/IP,然后通过实现一个 echo 服务器来学习 Java 的 Socket API。最后我们聊聊偏高级一点点的 socket 长连接和协议设计。

另外,本系列文章的前2篇《网络编程懒人入门(一):快速理解网络通信协议(上篇)》、《网络编程懒人入门(二):快速理解网络通信协议(下篇)》快速介绍了网络基本通信协议及理论基础,如果您对网络基础毫无概念,则请务必首先阅读完这2篇文章。本系列的第3篇文章《网络编程懒人入门(三):快速理解TCP协议一篇就够》有助于您快速理解TCP协议理论的方方面面,建议也可以读一读。

TCP 是互联网的核心协议之一,鉴于它的重要性,希望通过阅读上面介绍的几篇理论文章,再针对本文的动手实践,能真正加深您对TCP协议的理解。

如果您正打算系统地学习即时通讯开发,在读完本文后,建议您可以详细阅读《新手入门一篇就够:从零开发移动端IM》。

2、TCP/IP 协议简介

TCP/IP协议族是互联网最重要的基础设施之一,如有兴趣了解TCP/IP的贡献,可以读一读此文:《技术往事:改变世界的TCP/IP协议(珍贵多图、手机慎点)》,本文因篇幅原因仅作简要介绍。

2.1 IP协议

首先我们看 IP(Internet Protocol)协议。IP 协议提供了主机和主机间的通信。

为了完成不同主机的通信,我们需要某种方式来唯一标识一台主机,这个标识,就是著名的IP地址。通过IP地址,IP 协议就能够帮我们把一个数据包发送给对方。

2.2 TCP协议

前面我们说过,IP 协议提供了主机和主机间的通信。TCP 协议在 IP 协议提供的主机间通信功能的基础上,完成这两个主机上进程对进程的通信。

有了 IP,不同主机就能够交换数据。但是,计算机收到数据后,并不知道这个数据属于哪个进程(简单讲,进程就是一个正在运行的应用程序)。TCP 的作用就在于,让我们能够知道这个数据属于哪个进程,从而完成进程间的通信。

为了标识数据属于哪个进程,我们给需要进行 TCP 通信的进程分配一个唯一的数字来标识它。这个数字,就是我们常说的端口号。

TCP 的全称是 Transmission Control Protocol,大家对它说得最多的,大概就是面向连接的特性了。之所以说它是有连接的,是说在进行通信前,通信双方需要先经过一个三次握手的过程。三次握手完成后,连接便建立了。这时候我们才可以开始发送/接收数据。(与之相对的是 UDP,不需要经过握手,就可以直接发送数据)。

下面我们简单了解一下三次握手的过程:
网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接_1.png 

  • 首先,客户向服务端发送一个 SYN,假设此时 sequence number 为 x。这个 x 是由操作系统根据一定的规则生成的,不妨认为它是一个随机数;
  • 服务端收到 SYN 后,会向客户端再发送一个 SYN,此时服务器的 seq number = y。与此同时,会 ACK x+1,告诉客户端“已经收到了 SYN,可以发送数据了”;
  • 客户端收到服务器的 SYN 后,回复一个 ACK y+1,这个 ACK 则是告诉服务器,SYN 已经收到,服务器可以发送数据了。

经过这 3 步,TCP 连接就建立了,这里需要注意的有三点:

  • 连接是由客户端主动发起的;
  • 在第 3 步客户端向服务器回复 ACK 的时候,TCP 协议是允许我们携带数据的。之所以做不到,是 API 的限制导致的;
  • TCP 协议还允许 “四次握手” 的发生,同样的,由于 API 的限制,这个极端的情况并不会发生。

TCP/IP 相关的理论知识我们就先了解到这里,如果对TCP的3次握手和4次挥手还不太理解,那就详细读读以下文章:

  • 《通俗易懂-深入理解TCP协议(上):理论基础》
  • 《通俗易懂-深入理解TCP协议(下):RTT、滑动窗口、拥塞处理》
  • 《理论经典:TCP协议的3次握手与4次挥手过程详解》
  • 《理论联系实际:Wireshark抓包分析TCP 3次握手、4次挥手过程》

关于 TCP,还有诸如可靠性、流量控制、拥塞控制等非常有趣的特性。强烈推荐读者看一看 Richard 的名著《TCP/IP 详解 - 卷1》(注意,是第1版,不是第2版)。
网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接_2.jpg 
▲ 网络编程理论经典《TCP/IP 详解 - 卷1》在线阅读版点此进入

另外,TCP/IP协议其实是一个庞大的协议族,《计算机网络通讯协议关系图(中文珍藏版)》一文中为您清晰展现了这个协议族之间的关系,很有收藏价值,建议务必读一读。
网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接_3.png 
▲ TCP/IP协议族图高清原图点此进入

下面我们看一些偏实战的东西。

3、Socket 基本用法

Socket 是 TCP 层的封装,通过 socket,我们就能进行 TCP 通信。

在 Java 的 SDK 中,socket 的共有两个接口:用于监听客户连接的 ServerSocket 和用于通信的 Socket

使用 socket 的步骤如下:

  • 1)创建 ServerSocket 并监听客户连接;
  • 2)使用 Socket 连接服务端;
  • 3)通过 Socket.getInputStream()/getOutputStream() 获取输入输出流进行通信。

下面,我们通过实现一个简单的 echo 服务来学习 socket 的使用。所谓的 echo 服务,就是客户端向服务端写入任意数据,服务器都将数据原封不动地写回给客户端。

3.1 第一步:创建 ServerSocket 并监听客户连接

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public class EchoServer {

 

    private final ServerSocket mServerSocket;

 

    public EchoServer(int port) throws IOException {

        // 1. 创建一个 ServerSocket 并监听端口 port

        mServerSocket = new ServerSocket(port);

    }

 

    public void run() throws IOException {

        // 2. 开始接受客户连接

        Socket client = mServerSocket.accept();

        handleClient(client);

    }

 

    private void handleClient(Socket socket) {

        // 3. 使用 socket 进行通信 ...

    }

 

 

    public static void main(String[] argv) {

        try {

            EchoServer server = new EchoServer(9877);

            server.run();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

3.2 第二步:使用 Socket 连接服务端

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public class EchoClient {

 

    private final Socket mSocket;

 

    public EchoClient(String host, int port) throws IOException {

        // 创建 socket 并连接服务器

        mSocket = new Socket(host, port);

    }

 

    public void run() {

        // 和服务端进行通信

    }

 

 

    public static void main(String[] argv) {

        try {

            // 由于服务端运行在同一主机,这里我们使用 localhost

            EchoClient client = new EchoClient("localhost", 9877);

            client.run();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

3.3 第三步:通过 socket.getInputStream()/getOutputStream() 获取输入/输出流进行通信

首先,我们来实现服务端:

01

02

03

04

05

06

07

08

09

10

11

12

13

public class EchoServer {

    // ...

 

    private void handleClient(Socket socket) throws IOException {

        InputStream in = socket.getInputStream();

        OutputStream out = socket.getOutputStream();

        byte[] buffer = new byte[1024];

        int n;

        while ((n = in.read(buffer)) > 0) {

            out.write(buffer, 0, n);

        }

    }

}

可以看到,服务端的实现其实很简单,我们不停地读取输入数据,然后写回给客户端。

下面我们看看客户端:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

public class EchoClient {

    // ...

 

    public void run() throws IOException {

        Thread readerThread = new Thread(this::readResponse);

        readerThread.start();

 

        OutputStream out = mSocket.getOutputStream();

        byte[] buffer = new byte[1024];

        int n;

        while ((n = System.in.read(buffer)) > 0) {

            out.write(buffer, 0, n);

        }

    }

 

    private void readResponse() {

        try {

            InputStream in = mSocket.getInputStream();

            byte[] buffer = new byte[1024];

            int n;

            while ((n = in.read(buffer)) > 0) {

                System.out.write(buffer, 0, n);

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

客户端会稍微复杂一点点,在读取用户输入的同时,我们又想读取服务器的响应。所以,这里创建了一个线程来读服务器的响应。

不熟悉 lambda 的读者,可以把Thread readerThread = new Thread(this::readResponse) 换成下面这个代码:

1

2

3

4

5

6

Thread readerThread = new Thread(new Runnable() {

    @Override

    public void run() {

        readResponse();

    }

});

打开两个 terminal 分别执行如下命令:

1

2

3

4

5

6

7

8

9

$ javac EchoServer.java

$ java EchoServer

 

$ javac EchoClient.java

$ java EchoClient

hello Server

hello Server

foo

foo

在客户端,我们会看到,输入的所有字符都打印了出来。

3.4 最后需要注意的有几点

  • 1)在上面的代码中,我们所有的异常都没有处理。实际应用中,在发生异常时,需要关闭 socket,并根据实际业务做一些错误处理工作;
  • 2)在客户端,我们没有停止 readThread。实际应用中,我们可以通过关闭 socket 来让线程从阻塞读中返回。推荐读者阅读《Java并发编程实战》;
  • 3)我们的服务端只处理了一个客户连接。如果需要同时处理多个客户端,可以创建线程来处理请求。这个作为练习留给读者来完全。

4、Socket、ServerSocket 傻傻分不清楚

在进入这一节的主题前,读者不妨先考虑一个问题:在上一节的实例中,我们运行 echo 服务后,在客户端连接成功时,一个有多少个 socket 存在?

答案是 3 个 socket:客户端一个,服务端有两个。跟这个问题的答案直接关联的是本节的主题——Socket 和 ServerSocket 的区别是什么。

眼尖的读者,可能会注意到在上一节我是这样描述他们的:

在 Java 的 SDK 中,socket 的共有两个接口:用于监听客户连接的 ServerSocket 和用于通信的 Socket。

注意:我只说 ServerSocket 是用于监听客户连接,而没有说它也可以用来通信。下面我们来详细了解一下他们的区别。

注:以下描述使用的是 UNIX/Linux 系统的 API。

首先,我们创建 ServerSocket 后,内核会创建一个 socket。这个 socket 既可以拿来监听客户连接,也可以连接远端的服务。由于 ServerSocket 是用来监听客户连接的,紧接着它就会对内核创建的这个 socket 调用 listen 函数。这样一来,这个 socket 就成了所谓的 listening socket,它开始监听客户的连接。

接下来,我们的客户端创建一个 Socket,同样的,内核也创建一个 socket 实例。内核创建的这个 socket 跟 ServerSocket 一开始创建的那个没有什么区别。不同的是,接下来 Socket 会对它执行 connect,发起对服务端的连接。前面我们说过,socket API 其实是 TCP 层的封装,所以 connect 后,内核会发送一个 SYN 给服务端。

现在,我们切换角色到服务端。服务端的主机在收到这个 SYN 后,会创建一个新的 socket,这个新创建的 socket 跟客户端继续执行三次握手过程。

三次握手完成后,我们执行的 serverSocket.accept() 会返回一个 Socket 实例,这个 socket 就是上一步内核自动帮我们创建的。

所以说:在一个客户端连接的情况下,其实有 3 个 socket。

关于内核自动创建的这个 socket,还有一个很有意思的地方。它的端口号跟 ServerSocket 是一毛一样的。咦!!不是说,一个端口只能绑定一个 socket 吗?其实这个说法并不够准确。

前面我说的TCP 通过端口号来区分数据属于哪个进程的说法,在 socket 的实现里需要改一改。Socket 并不仅仅使用端口号来区别不同的 socket 实例,而是使用 <peer addr:peer port, local addr:local port> 这个四元组。

在上面的例子中,我们的 ServerSocket 长这样:<*:*, *:9877>。意思是,可以接受任何的客户端,和本地任何 IP。

accept 返回的 Socket 则是这样:<127.0.0.1:xxxx, 127.0.0.1:9877>。其中,xxxx 是客户端的端口号。

如果数据是发送给一个已连接的 socket,内核会找到一个完全匹配的实例,所以数据准确发送给了对端。

如果是客户端要发起连接,这时候只有 <*:*, *:9877> 会匹配成功,所以 SYN 也准确发送给了监听套接字。

Socket/ServerSocket 的区别我们就讲到这里。如果读者觉得不过瘾,可以参考《TCP/IP 详解》卷1、卷2。

5、Socket “长”连接的实现

5.1 背景知识

Socket 长连接,指的是在客户和服务端之间保持一个 socket 连接长时间不断开。

比较熟悉 Socket 的读者,可能知道有这样一个 API:

1

socket.setKeepAlive(true);

嗯……keep alive,“保持活着”,这个应该就是让 TCP 不断开的意思。那么,我们要实现一个 socket 的长连接,只需要这一个调用即可。

遗憾的是,生活并不总是那么美好。对于 4.4BSD 的实现来说,Socket 的这个 keep alive 选项如果打开并且两个小时内没有通信,那么底层会发一个心跳,看看对方是不是还活着。

注意:两个小时才会发一次。也就是说,在没有实际数据通信的时候,我把网线拔了,你的应用程序要经过两个小时才会知道

这个话题,对于即时通讯的老手来说,也就是经常讨论的“网络连接心跳保活”这个话题了,感兴趣的话可以读一读《聊聊iOS中网络编程长连接的那些事》、《为何基于TCP协议的移动端IM仍然需要心跳保活机制?》、《微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)》、《Android端消息推送总结:实现原理、心跳保活、遇到的问题等》。

在说明如果实现长连接前,我们先来理一理我们面临的问题。

假定现在有一对已经连接的 socket,在以下情况发生时候,socket 将不再可用:

  • 1)某一端关闭是 socket(这不是废话吗):主动关闭的一方会发送 FIN,通知对方要关闭 TCP 连接。在这种情况下,另一端如果去读 socket,将会读到 EoF(End of File)。于是我们知道对方关闭了 socket;
  • 2)应用程序奔溃:此时 socket 会由内核关闭,结果跟情况1一样;
  • 3)系统奔溃:这时候系统是来不及发送 FIN 的,因为它已经跪了。此时对方无法得知这一情况。对方在尝试读取数据时,最后会返回 read time out。如果写数据,则是 host unreachable 之类的错误。
  • 4)电缆被挖断、网线被拔:跟情况3差不多,如果没有对 socket 进行读写,两边都不知道发生了事故。跟情况3不同的是,如果我们把网线接回去,socket 依旧可以正常使用。

在上面的几种情形中,有一个共同点就是,只要去读、写 socket,只要 socket 连接不正常,我们就能够知道。基于这一点,要实现一个 socket 长连接,我们需要做的就是不断地给对方写数据,然后读取对方的数据,也就是所谓的心跳。只要心还在跳,socket 就是活的。写数据的间隔,需要根据实际的应用需求来决定。

心跳包不是实际的业务数据,根据通信协议的不同,需要做不同的处理。

比方说,我们使用 JSON 进行通信,那么,可以为协议包加一个 type 字段,表面这个 JSON 是心跳还是业务数据:

1

2

3

4

5

{

    "type": 0,  // 0 表示心跳

 

    // ...

}

使用二进制协议的情况类似。要求就是,我们能够区别一个数据包是心跳还是真实数据。这样,我们便实现了一个 socket 长连接。

5.2 实现示例

这一小节我们一起来实现一个带长连接的 Android echo 客户端。完整的代码可以在本文末尾的附件找到

首先了接口部分:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

public final class LongLiveSocket {

 

    /**

     * 错误回调

     */

    public interface ErrorCallback {

        /**

         * 如果需要重连,返回 true

         */

        boolean onError();

    }

 

 

    /**

     * 读数据回调

     */

    public interface DataCallback {

        void onData(byte[] data, int offset, int len);

    }

 

 

    /**

     * 写数据回调

     */

    public interface WritingCallback {

        void onSuccess();

        void onFail(byte[] data, int offset, int len);

    }

 

 

    public LongLiveSocket(String host, int port,

                          DataCallback dataCallback, ErrorCallback errorCallback) {

    }

 

    public void write(byte[] data, WritingCallback callback) {

    }

 

    public void write(byte[] data, int offset, int len, WritingCallback callback) {

    }

 

    public void close() {

    }

}

 

我们这个支持长连接的类就叫 LongLiveSocket 好了。如果在 socket 断开后需要重连,只需要在对应的接口里面返回 true 即可(在真实场景里,我们还需要让客户设置重连的等待时间,还有读写、连接的 timeout等。为了简单,这里就直接不支持了。

另外需要注意的一点是,如果要做一个完整的库,需要同时提供阻塞式和回调式API。同样由于篇幅原因,这里直接省掉了。

下面我们直接看实现:

001

002

003

004

005

006

007

008

009

010

011

012

013

014

015

016

017

018

019

020

021

022

023

024

025

026

027

028

029

030

031

032

033

034

035

036

037

038

039

040

041

042

043

044

045

046

047

048

049

050

051

052

053

054

055

056

057

058

059

060

061

062

063

064

065

066

067

068

069

070

071

072

073

074

075

076

077

078

079

080

081

082

083

084

085

086

087

088

089

090

091

092

093

094

095

096

097

098

099

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

public final class LongLiveSocket {

    private static final String TAG = "LongLiveSocket";

 

    private static final long RETRY_INTERVAL_MILLIS = 3 * 1000;

    private static final long HEART_BEAT_INTERVAL_MILLIS = 5 * 1000;

    private static final long HEART_BEAT_TIMEOUT_MILLIS = 2 * 1000;

 

    /**

     * 错误回调

     */

    public interface ErrorCallback {

        /**

         * 如果需要重连,返回 true

         */

        boolean onError();

    }

 

 

    /**

     * 读数据回调

     */

    public interface DataCallback {

        void onData(byte[] data, int offset, int len);

    }

 

 

    /**

     * 写数据回调

     */

    public interface WritingCallback {

        void onSuccess();

        void onFail(byte[] data, int offset, int len);

    }

 

 

    private final String mHost;

    private final int mPort;

    private final DataCallback mDataCallback;

    private final ErrorCallback mErrorCallback;

 

    private final HandlerThread mWriterThread;

    private final Handler mWriterHandler;

    private final Handler mUIHandler = new Handler(Looper.getMainLooper());

 

    private final Object mLock = new Object();

    private Socket mSocket;  // guarded by mLock

    private boolean mClosed; // guarded by mLock

 

    private final Runnable mHeartBeatTask = new Runnable() {

        private byte[] mHeartBeat = new byte[0];

 

        @Override

        public void run() {

            // 我们使用长度为 0 的数据作为 heart beat

            write(mHeartBeat, new WritingCallback() {

                @Override

                public void onSuccess() {

                    // 每隔 HEART_BEAT_INTERVAL_MILLIS 发送一次

                    mWriterHandler.postDelayed(mHeartBeatTask, HEART_BEAT_INTERVAL_MILLIS);

                    mUIHandler.postDelayed(mHeartBeatTimeoutTask, HEART_BEAT_TIMEOUT_MILLIS);

                }

 

                @Override

                public void onFail(byte[] data, int offset, int len) {

                    // nop

                    // write() 方法会处理失败

                }

            });

        }

    };

 

    private final Runnable mHeartBeatTimeoutTask = () -> {

        Log.e(TAG, "mHeartBeatTimeoutTask#run: heart beat timeout");

        closeSocket();

    };

 

 

    public LongLiveSocket(String host, int port,

                          DataCallback dataCallback, ErrorCallback errorCallback) {

        mHost = host;

        mPort = port;

        mDataCallback = dataCallback;

        mErrorCallback = errorCallback;

 

        mWriterThread = new HandlerThread("socket-writer");

        mWriterThread.start();

        mWriterHandler = new Handler(mWriterThread.getLooper());

        mWriterHandler.post(this::initSocket);

    }

 

    private void initSocket() {

        while (true) {

            if (closed()) return;

 

            try {

                Socket socket = new Socket(mHost, mPort);

                synchronized (mLock) {

                    // 在我们创建 socket 的时候,客户可能就调用了 close()

                    if (mClosed) {

                        silentlyClose(socket);

                        return;

                    }

                    mSocket = socket;

                    // 每次创建新的 socket,会开一个线程来读数据

                    Thread reader = new Thread(new ReaderTask(socket), "socket-reader");

                    reader.start();

                    mWriterHandler.post(mHeartBeatTask);

                }

                break;

            } catch (IOException e) {

                Log.e(TAG, "initSocket: ", e);

                if (closed() || !mErrorCallback.onError()) {

                    break;

                }

                try {

                    TimeUnit.MILLISECONDS.sleep(RETRY_INTERVAL_MILLIS);

                } catch (InterruptedException e1) {

                    // interrupt writer-thread to quit

                    break;

                }

            }

        }

    }

 

    public void write(byte[] data, WritingCallback callback) {

        write(data, 0, data.length, callback);

    }

 

    public void write(byte[] data, int offset, int len, WritingCallback callback) {

        mWriterHandler.post(() -> {

            Socket socket = getSocket();

            if (socket == null) {

                // initSocket 失败而客户说不需要重连,但客户又叫我们给他发送数据

                throw new IllegalStateException("Socket not initialized");

            }

            try {

                OutputStream outputStream = socket.getOutputStream();

                DataOutputStream out = new DataOutputStream(outputStream);

                out.writeInt(len);

                out.write(data, offset, len);

                callback.onSuccess();

            } catch (IOException e) {

                Log.e(TAG, "write: ", e);

                closeSocket();

                callback.onFail(data, offset, len);

                if (!closed() && mErrorCallback.onError()) {

                    initSocket();

                }

            }

        });

    }

 

    private boolean closed() {

        synchronized (mLock) {

            return mClosed;

        }

    }

 

    private Socket getSocket() {

        synchronized (mLock) {

            return mSocket;

        }

    }

 

    private void closeSocket() {

        synchronized (mLock) {

            closeSocketLocked();

        }

    }

 

    private void closeSocketLocked() {

        if (mSocket == null) return;

 

        silentlyClose(mSocket);

        mSocket = null;

        mWriterHandler.removeCallbacks(mHeartBeatTask);

    }

 

    public void close() {

        if (Looper.getMainLooper() == Looper.myLooper()) {

            new Thread() {

                @Override

                public void run() {

                    doClose();

                }

            }.start();

        } else {

            doClose();

        }

    }

 

    private void doClose() {

        synchronized (mLock) {

            mClosed = true;

            // 关闭 socket,从而使得阻塞在 socket 上的线程返回

            closeSocketLocked();

        }

        mWriterThread.quit();

        // 在重连的时候,有个 sleep

        mWriterThread.interrupt();

    }

 

 

    private static void silentlyClose(Closeable closeable) {

        if (closeable != null) {

            try {

                closeable.close();

            } catch (IOException e) {

                Log.e(TAG, "silentlyClose: ", e);

                // error ignored

            }

        }

    }

 

 

    private class ReaderTask implements Runnable {

 

        private final Socket mSocket;

 

        public ReaderTask(Socket socket) {

            mSocket = socket;

        }

 

        @Override

        public void run() {

            try {

                readResponse();

            } catch (IOException e) {

                Log.e(TAG, "ReaderTask#run: ", e);

            }

        }

 

        private void readResponse() throws IOException {

            // For simplicity, assume that a msg will not exceed 1024-byte

            byte[] buffer = new byte[1024];

            InputStream inputStream = mSocket.getInputStream();

            DataInputStream in = new DataInputStream(inputStream);

            while (true) {

                int nbyte = in.readInt();

                if (nbyte == 0) {

                    Log.i(TAG, "readResponse: heart beat received");

                    mUIHandler.removeCallbacks(mHeartBeatTimeoutTask);

                    continue;

                }

 

                if (nbyte > buffer.length) {

                    throw new IllegalStateException("Receive message with len " + nbyte +

                                    " which exceeds limit " + buffer.length);

                }

 

                if (readn(in, buffer, nbyte) != 0) {

                    // Socket might be closed twice but it does no harm

                    silentlyClose(mSocket);

                    // Socket will be re-connected by writer-thread if you want

                    break;

                }

                mDataCallback.onData(buffer, 0, nbyte);

            }

        }

 

        private int readn(InputStream in, byte[] buffer, int n) throws IOException {

            int offset = 0;

            while (n > 0) {

                int readBytes = in.read(buffer, offset, n);

                if (readBytes < 0) {

                    // EoF

                    break;

                }

                n -= readBytes;

                offset += readBytes;

            }

            return n;

        }

    }

}

下面是我们新实现的 EchoClient:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public class EchoClient {

    private static final String TAG = "EchoClient";

 

    private final LongLiveSocket mLongLiveSocket;

 

    public EchoClient(String host, int port) {

        mLongLiveSocket = new LongLiveSocket(

                host, port,

                (data, offset, len) -> Log.i(TAG, "EchoClient: received: " + new String(data, offset, len)),

                // 返回 true,所以只要出错,就会一直重连

                () -> true);

    }

 

    public void send(String msg) {

        mLongLiveSocket.write(msg.getBytes(), new LongLiveSocket.WritingCallback() {

            @Override

            public void onSuccess() {

                Log.d(TAG, "onSuccess: ");

            }

 

            @Override

            public void onFail(byte[] data, int offset, int len) {

                Log.w(TAG, "onFail: fail to write: " + new String(data, offset, len));

                // 连接成功后,还会发送这个消息

                mLongLiveSocket.write(data, offset, len, this);

            }

        });

    }

}

就这样,一个带 socket 长连接的客户端就完成了。剩余代码跟我们这里的主题没有太大关系,感兴趣的读者可以看看文末附件里的源码或者自己完成这个例子。

下面是一些输出示例:

01

02

03

04

05

06

07

08

09

10

11

03:54:55.583 12691-12713/com.example.echo I/LongLiveSocket: readResponse: heart beat received

03:55:00.588 12691-12713/com.example.echo I/LongLiveSocket: readResponse: heart beat received

03:55:05.594 12691-12713/com.example.echo I/LongLiveSocket: readResponse: heart beat received

03:55:09.638 12691-12710/com.example.echo D/EchoClient: onSuccess:

03:55:09.639 12691-12713/com.example.echo I/EchoClient: EchoClient: received: hello

03:55:10.595 12691-12713/com.example.echo I/LongLiveSocket: readResponse: heart beat received

03:55:14.652 12691-12710/com.example.echo D/EchoClient: onSuccess:

03:55:14.654 12691-12713/com.example.echo I/EchoClient: EchoClient: received: echo

03:55:15.596 12691-12713/com.example.echo I/LongLiveSocket: readResponse: heart beat received

03:55:20.597 12691-12713/com.example.echo I/LongLiveSocket: readResponse: heart beat received

03:55:25.602 12691-12713/com.example.echo I/LongLiveSocket: readResponse: heart beat received

最后需要说明的是,如果想节省资源,在有客户发送数据的时候可以省略 heart beat。

我们对读出错时候的处理,可能也存在一些争议。读出错后,我们只是关闭了 socket。socket 需要等到下一次写动作发生时,才会重新连接。实际应用中,如果这是一个问题,在读出错后可以直接开始重连。这种情况下,还需要一些额外的同步,避免重复创建 socket。heart beat timeout 的情况类似。

6、跟 TCP/IP 学协议设计

如果仅仅是为了使用是 socket,我们大可以不去理会协议的细节。之所以推荐大家去看一看《TCP/IP 详解》,是因为它们有太多值得学习的地方。很多我们工作中遇到的问题,都可以在这里找到答案。

以下每一个小节的标题都是一个小问题,建议读者独立思考一下,再继续往下看

6.1 协议版本如何升级?

有这么一句流行的话:这个世界唯一不变的,就是变化。当我们对协议版本进行升级的时候,正确识别不同版本的协议对软件的兼容非常重要。那么,我们如何设计协议,才能够为将来的版本升级做准备呢?

答案可以在 IP 协议找到。

IP 协议的第一个字段叫 version,目前使用的是 4 或 6,分别表示 IPv4 和 IPv6。由于这个字段在协议的开头,接收端收到数据后,只要根据第一个字段的值就能够判断这个数据包是 IPv4 还是 IPv6。

再强调一下,这个字段在两个版本的IP协议都位于第一个字段,为了做兼容处理,对应的这个字段必须位于同一位置。文本协议(如,JSON、HTML)的情况类似。

6.2 如何发送不定长数据的数据包?

举个例子,我们用微信发送一条消息。这条消息的长度是不确定的,并且每条消息都有它的边界。我们如何来处理这个边界呢?

还是一样,看看 IP。IP 的头部有个 header length 和 data length 两个字段。通过添加一个 len 域,我们就能够把数据根据应用逻辑分开。

跟这个相对的,还有另一个方案,那就是在数据的末尾放置终止符。比方说,想 C 语言的字符串那样,我们在每个数据的末尾放一个 \0 作为终止符,用以标识一条消息的尾部。这个方法带来的问题是,用户的数据也可能存在 \0。此时,我们就需要对用户的数据进行转义。比方说,把用户数据的所有 \0 都变成 \0\0。读消息的过程总,如果遇到 \0\0,那它就代表 \0,如果只有一个 \0,那就是消息尾部。

使用 len 字段的好处是,我们不需要对数据进行转义。读取数据的时候,只要根据 len 字段,一次性把数据都读进来就好,效率会更高一些。

终止符的方案虽然要求我们对数据进行扫描,但是如果我们可能从任意地方开始读取数据,就需要这个终止符来确定哪里才是消息的开头了。

当然,这两个方法不是互斥的,可以一起使用。

6.3上传多个文件,只有所有文件都上传成功时才算成功

现在我们有一个需求,需要一次上传多个文件到服务器,只有在所有文件都上传成功的情况下,才算成功。我们该如何来实现呢?

IP 在数据报过大的时候,会把一个数据报拆分成多个,并设置一个 MF (more fragments)位,表示这个包只是被拆分后的数据的一部分。

好,我们也学一学 IP。这里,我们可以给每个文件从 0 开始编号。上传文件的同时,也携带这个编号,并额外附带一个 MF 标志。除了编号最大的文件,所有文件的 MF 标志都置位。因为 MF 没有置位的是最后一个文件,服务器就可以根据这个得出总共有多少个文件。

另一种不使用 MF 标志的方法是,我们在上传文件前,就告诉服务器总共有多少个文件。

如果读者对数据库比较熟悉,学数据库用事务来处理,也是可以的。这里就不展开讨论了。

6.4 如何保证数据的有序性?

这里讲一个我曾经遇到过的面试题。现在有一个任务队列,多个工作线程从中取出任务并执行,执行结果放到一个结果队列中。先要求,放入结果队列的时候,顺序顺序需要跟从工作队列取出时的一样(也就是说,先取出的任务,执行结果需要先放入结果队列)。

我们看看 TCP/IP 是怎么处理的。IP 在发送数据的时候,不同数据报到达对端的时间是不确定的,后面发送的数据有可能较先到达。TCP 为了解决这个问题,给所发送数据的每个字节都赋了一个序列号,通过这个序列号,TCP 就能够把数据按原顺序重新组装。

一样,我们也给每个任务赋一个值,根据进入工作队列的顺序依次递增。工作线程完成任务后,在将结果放入结果队列前,先检查要放入对象的写一个序列号是不是跟自己的任务相同,如果不同,这个结果就不能放进去。此时,最简单的做法是等待,知道下一个可以放入队列的结果是自己所执行的那一个。但是,这个线程就没办法继续处理任务了。

更好的方法是,我们维护多一个结果队列的缓冲,这个缓冲里面的数据按序列号从小到大排序。

工作线程要将结果放入,有两种可能:

  • 1)刚刚完成的任务刚好是下一个,将这个结果放入队列。然后从缓冲的头部开始,将所有可以放入结果队列的数据都放进去;
  • 2)所完成的任务不能放入结果队列,这个时候就插入结果队列。然后,跟上一种情况一样,需要检查缓冲。

如果测试表明,这个结果缓冲的数据不多,那么使用普通的链表就可以。如果数据比较多,可以使用一个最小堆。

6.5 如何保证对方收到了消息?

我们说,TCP 提供了可靠的传输。这样不就能够保证对方收到消息了吗?

很遗憾,其实不能。在我们往 socket 写入的数据,只要对端的内核收到后,就会返回 ACK,此时,socket 就认为数据已经写入成功。然而要注意的是,这里只是对方所运行的系统的内核成功收到了数据,并不表示应用程序已经成功处理了数据。

解决办法还是一样,我们学 TCP,添加一个应用层的 APP ACK。应用接收到消息并处理成功后,发送一个 APP ACK 给对方。

有了 APP ACK,我们需要处理的另一个问题是,如果对方真的没有收到,需要怎么做?

TCP 发送数据的时候,消息一样可能丢失。TCP 发送数据后,如果长时间没有收到对方的 ACK,就假设数据已经丢失,并重新发送。

我们也一样,如果长时间没有收到 APP ACK,就假设数据丢失,重新发送一个。

关于数据送达保证和应应答机制,以下文章进行了详细讨论:

  • 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》
  • 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》
  • 《IM群聊消息如此复杂,如何保证不丢不重?》
  • 《从客户端的角度来谈谈移动端IM的消息可靠性和送达机制》

 

9、源码附件下载


 手把手教你写基于TCP的Socket长连接-源码(52im.net).zip (142.48 KB , 下载次数: 55 , 售价: 1 金币)

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 【深度学习 激活函数】激活函数tensorflow使用宝典

    无意中发现了一个巨牛的人工智能教程,忍不住分享一下给大家。教程不仅是零基础,通俗易懂,而且非常风趣幽默,像看小说一样!觉得太牛了,所以分享给大家。点这里可以跳转到教程。人工智能教程 1、总体效果比较ELU > leaky ReLU(及其变体)> ReLU > tanh > sigm…...

    2024/4/27 18:32:16
  2. 神啊,赐个妞吧!

    兄弟们,闷阿!!神啊,赐个妞吧 神啊,赐个妞吧!神啊,赐个妞吧!    “你确实很不错,帅而有型,英俊潇洒,玉树临风,谈吐不凡,见识广博,幽默风趣,善解人意,聪明伶俐,体格健美,一表人才,神采飞扬。”姑妈话锋一转,“但你也不能这么大了还不找个老婆啊!”  …...

    2024/4/17 8:41:56
  3. 诡异,跟家人完全失去联系!

    昨天给老爸打了两次电话,关机。。。 现在,中午了,老爸肯定下课了,还是关机。。。 打大姐的手机。。。。“此号码禁止呼入”。。。。诡异! 打大姐家里的坐机,。。。“the number is not existed ,please check it in dial later!”。。。非常诡异! 打大姐夫的手机。。。…...

    2024/4/27 16:19:44
  4. hbase导出数据

    HBase数据导出到本地文件 hbase org.apache.hadoop.hbase.mapreduce.Export 表名 file:///Users/a6/Applications/experiment_data/hbase_data/bak HBase数据导出到hdfs hbase org.apache.hadoop.hbase.mapreduce.Export 表名 /hbase/emp_bak...

    2024/3/31 9:48:48
  5. 《线性代数》 李炯生\查建国\王新茂 中国科学技术大学 第2版 部分习题答案

    百度了一圈没有靠谱点的答案,于是便有了这篇博客。 只写了一些自己觉得有价值的习题。 文章目录第一章 多项式1.3 整除性与最大公因式习题3习题101.5 实系数与复系数多项式习题1习题2.(4)习题101.6 整系数与有理系数多项式例1例2习题2习题6习题7习题81.8 对称多项式习题3习题6…...

    2024/4/20 7:05:57
  6. 手把手教你详细的硬件电路设计

    本文转自 | 巧学数电模电单片机这篇文章献给那些刚开始或即将开始设计硬件电路的人,刚刚开始接触电路板的时候,你可能充满了疑惑同时又带着些兴奋。在网上许多关于硬件电路的经验知识让人目不暇接,像信号完整性、EMI准会把你搞晕,别急,一切要慢慢来。01总体思路设计硬件电…...

    2024/4/27 16:40:53
  7. 笑话段子

    少妇报案:“我把钱放在胸衣内,在拥挤的地铁内被一帅哥偷走了…”警察纳闷:“这么敏感的地方你就没觉察到?”少妇红着脸答:“谁能想到他是摸钱呢?一女对出轨的老公说:你如果敢离婚娶那个年轻的妖精,我就嫁给妖精她爹,从此以后,儿子管你叫姐夫,你得喊我妈!老公当场晕…...

    2024/4/27 15:19:09
  8. 三大前端框架开发环境的安装步骤

    Vue npm install @vue/cli -g vue create hello_world React npm install reactweb-cli -g reactweb init hello_world Angular npm install @angular/cli -g ng new hello_world...

    2024/4/27 13:25:18
  9. 编码器的计算线性代数审查

    编码器的计算线性代数审查作者:杰森布朗利时间:2018年3月21日数值线性代数关注的是实现和执行的实际意义与真实数据矩阵运算的计算机。这是一个需要经验的线性代数,其重点是操作的性能和精度。该公司发展速度很快。ai发布了一个免费的课程名为“计算线性代数”数值线性代数的…...

    2024/4/27 18:05:57
  10. ZOJ 1133 Smith Numbers && POJ 1142 Smith Numbers

    史密斯数美国有一位数字家名叫阿尔伯特威兰斯基,他姐夫史密斯非常喜欢研究数学,所以两人经常在一起研讨各种数学问题。有时,两人碰不到一起,就习惯性地用电话交流。 一天,两人刚结束电话交谈,史密斯突然灵感来临,对威兰斯基的电话号码“4937775”产生 了兴趣,总觉得这是…...

    2024/4/27 15:24:16
  11. 手把手教你写一个RPC

    1.1 RPC 是什么 定义:RPC(Remote Procedure Call Protocol)——远程过程调用协议 ,RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层 ,RPC使得开发包括网络分布式多程序在内的应用程序更加容易。 我…...

    2024/4/4 22:44:55
  12. Hibernate二级缓存,使用Ehache缓存框架

    1、前言二级缓存是属于SessionFactory级别的缓存机制,是属于进程范围的缓存。一级缓存是Session级别的缓存,是属于事务范围的缓存,由Hibernate管理,一般无需进行干预。 Hibernate支持以下的第三方的缓存框架:CacheInterfaceSupported strategiesHashTable (testing only)r…...

    2024/4/17 8:41:32
  13. JS数组三

    title: JS数组三 date: 2020-06-13 17:43:17 tags: - JavaScripe查漏补缺 - sort(),reduce() 关于sort()方法接受的是一个函数,返回排序后的数组let arr=[9,5,2,7] let result = arr.sort(function (a,b) { //return a-b//返回负数从小到大,[2, 5, 7, 9] //return b-a//返回正…...

    2024/4/27 18:13:34
  14. ESR五部曲之四——Homesteading the Noosphere 开拓智域

    开拓智域 Eric S. Raymond五部曲之Homesteading the Noosphereby Eric S. RaymondApril 1998 --------------------------------------------------------------------------------在 观察由开放原始码版权所定义的"官方"意识形态与真正玩家的行为後, 发现到一些矛盾…...

    2024/4/17 8:42:32
  15. 「Deep Learning」Note on ELU(Exponential Linear Unit)

    Sina Weibo:小锋子Shawn Tencent E-mail:403568338@qq.com http://blog.csdn.net/dgyuanshaofeng/article/details/79952331[1] Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs)...

    2024/4/17 8:41:44
  16. hibernate二级缓存中清空set方法

    Hibernate的二级缓存2008-03-25 14:52Hibernate的Session在事务级别进行持久化数据的缓存操作。 当然,也有可能分别为每个类(或集合),配置集群、或JVM级别(SessionFactory级别)的缓存。 你甚至可以为之插入一个集群的缓存。注意,缓存永远不知道其他应用程序对持久化仓库(数据…...

    2024/4/19 12:43:23
  17. 资料 | 线性代数不在枯燥,可视化交互式学习笔记

    最近,美国佐治亚理工学院也推出了一本可交互的线性代数书,用大量的图和文字可视化地介绍了线性代数知识。该书是佐治亚理工Math 1553的配套教材,共有455页,包含了140多个交互demo,可以让读者直观地理解代数背后的几何性质。这本书是半计算、半概念性质的,主要目标是提供一…...

    2024/4/18 9:54:07
  18. 英语汉语对比学习:名词(二)

    常用复数的名词 有些东西常是成对出现的话我们就是一直用复数形式.比如 trousers 长裤 shorts短裤 scissors 剪刀 sunglasses太阳镜 compasses圆规名词做定语 按我们的思维习惯,一般名词做定语时要用单数,有些英语词确实是这样,但有 些又是用复数.而没办…...

    2024/3/31 20:13:57
  19. 开拓智域-Eric Raymond

    开拓智域by Eric S. RaymondApril 1998 这是我继教堂观与市集观後的续集. 在该文中, 我详细地检视在开放原始码文化中的财产及所有权传承. 是的, 它确实有财产传承 -- 而且其中非常的精巧, 这揭露出在台面下的礼物文化, 玩家们友善地竞争同跻之间相互尊重的名望. 这份分析对於…...

    2024/4/17 8:41:26
  20. 深度学习—激活函数详解(Sigmoid、tanh、ReLU、ReLU6及变体P-R-Leaky、ELU、SELU、Swish、Mish、Maxout、hard-sigmoid、hard-swish)

    非线性激活函数详解饱和激活函数Sigmoid函数tanh函数hard-Sigmoid函数非饱和激活函数Relu(修正线性单元):Relu6(抑制其最大值):ELU(指数线性单元)SELULeaky-Relu / R-ReluP-Relu(参数化修正线性单元)R-Relu(随机纠正线性单元)Swishhard-SwishMishMaxout关于激活函数…...

    2024/4/17 8:42:44

最新文章

  1. 【Godot4.2】有序和无序列表函数库 - myList

    概述 在打印输出或其他地方可能需要构建有序或无序列表。本质就是构造和维护一个纯文本数组。并用格式化文本形式&#xff0c;输出带序号或前缀字符的多行文本。 为此我专门设计了一个类myList&#xff0c;来完成这项任务。 代码 以下是myList类的完整代码&#xff1a; # …...

    2024/4/27 18:38:59
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. STM32重要参考资料

    stm32f103c8t6 一、引脚定义图 二、时钟树 三、系统结构图 四、启动配置 &#xff08;有时候不小心短接VCC和GND&#xff0c;芯片会锁住&#xff0c;可以BOOT0拉高试试&#xff08;用跳线帽接&#xff09;&#xff09; 五、最小系统原理图 可用于PCB设计 六、常见折腾人bug…...

    2024/4/26 8:20:13
  4. springboot 项目整合easy-captcha验证码功能

    效果 1、验证码使用easy-captcha,在pom文件增加依赖 <!-- google 验证码 --><dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId></dependency> 2、增加获取kaptcha的ctrl package com.*.*.s…...

    2024/4/24 13:16:37
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/4/26 18:09:39
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/26 20:12:18
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/26 23:05:52
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/27 4:00:35
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/26 21:56:58
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/27 9:01:45
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/25 18:39:16
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/25 18:39:16
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/26 22:01:59
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/25 18:39:14
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/4/26 23:04:58
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/25 2:10:52
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/25 18:39:00
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/26 19:46:12
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/27 11:43:08
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/27 8:32:30
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57