同步和异步,阻塞和非阻塞

最近的传输项目中,性能一直上不去,考虑着项目本身存在的问题,除了编码之外,找来找去也想不出个所以然来,现在才知道基础理论知识有多重要。因为几个简单的问题就让我困扰:

什么是阻塞和非阻塞;

什么是同步和异步;

FTP传输在局域网中为什么这么快;

C/S下采用什么样的模式才能更快,或者客户端怎么样才能发的更快;

每次传多少才能到最大的性能。

……

今天找了大神分析了下项目的代码,拆包压缩的工作太频繁,而且每个切片都做一次checksum,导致性能上不去,这个之前也想到了。而真正的问题我觉得还是出在了通信模式上。到底客户端怎么发数据才能最快。到底在客户端要不要用非阻塞。而讲清楚这些,那四个概念才是重点。我们先从通信上解释同步和异步:

异步传输(Asynchronous Transmission): 异步传输将比特分成小组进行传送,小组可以是8位的1个字符或更长。发送方可以在任何时刻发送这些比特组,而接收方从不知道它们会在什么时候到达。一个常见的例子是计算机键盘与主机的通信。按下一个字母键、数字键或特殊字符键,就发送一个8比特位的ASCII代码。键盘可以在任何时刻发送代码,这取决于用户的输入速度,内部的硬件必须能够在任何时刻接收一个键入的字符。

异步传输存在一个潜在的问题,即接收方并不知道数据会在什么时候到达。在它检测到数据并做出响应之前,第一个比特已经过去了。这就像有人出乎意料地从后面走上来跟你说话,而你没来得及反应过来,漏掉了最前面的几个词。因此,每次异步传输的信息都以一个起始位开头,它通知接收方数据已经到达了,这就给了接收方响应、接收和缓存数据比特的时间;在传输结束时,一个停止位表示该次传输信息的终止。按照惯例,空闲(没有传送数据)的线路实际携带着一个代表二进制1的信号,异步传输的开始位使信号变成0,其他的比特位使信号随传输的数据信息而变化。最后,停止位使信号重新变回1,该信号一直保持到下一个开始位到达。例如在键盘上数字“1”,按照8比特位的扩展ASCII编码,将发送“00110001”,同时需要在8比特位的前面加一个起始位,后面一个停止位。

异步传输的实现比较容易,由于每个信息都加上了“同步”信息,因此计时的漂移不会产生大的积累,但却产生了较多的开销。在上面的例子,每8个比特要多传送两个比特,总的传输负载就增加25%。对于数据传输量很小的低速设备来说问题不大,但对于那些数据传输量很大的高速设备来说,25%的负载增值就相当严重了。因此,异步传输常用于低速设备。

同步传输(Synchronous Transmission):同步传输的比特分组要大得多。它不是独立地发送每个字符,每个字符都有自己的开始位和停止位,而是把它们组合起来一起发送。我们将这些组合称为数据帧,或简称为帧。
 
同步传输通常要比异步传输快速得多。接收方不必对每个字符进行开始和停止的操作。一旦检测到帧同步字符,它就在接下来的数据到达时接收它们。另外,同步传输的开销也比较少。例如,一个典型的帧可能有500字节(即4000比特)的数据,其中可能只包含100比特的开销。这时,增加的比特位使传输的比特总数增加2.5%,这与异步传输中25 %的增值要小得多。随着数据帧中实际数据比特位的增加,开销比特所占的百分比将相应地减少。但是,数据比特位越长,缓存数据所需要的缓冲区也越大,这就限制了一个帧的大小。另外,帧越大,它占据传输媒体的连续时间也越长。在极端的情况下,这将导致其他用户等得太久。
 
同步传输方式中发送方和接收方的时钟是统一的、字符与字符间的传输是同步无间隔的。
异步传输方式并不要求发送方和接收方的时钟完全一样,字符与字符间的传输是异步的。
同步与异步传输的区别
1,异步传输是面向字符的传输,而同步传输是面向比特的传输。
2,异步传输的单位是字符而同步传输的单位是桢。
3,异步传输通过字符起止的开始和停止码抓住再同步的机会,而同步传输则是以数据中抽取同步信息。
4,异步传输对时序的要求较低,同步传输往往通过特定的时钟线路协调时序。
5,异步传输相对于同步传输效率较低。

阻塞是指客户端向服务器端发起请求,每次只能有一个连接被占用,其余连接在当前线程没有处理完之前都要等待。反之亦然。

非阻塞也容易理解,一个线程可以负载多个连接,新来的客户端可以不用等待。

It's important to understand the difference between those two APIs. BIO, or Blocking IO, relies on plain sockets used in a blocking mode : when you read, write or do whatever operation on a socket, the called operation will blcok the caller until the operation is completed.

In some cases, it's critical to be able to call the operation, and to expect the called operation to inform the caller when the operation is done : the caller can then do something else in the mean time.

This is also where NIO offers a better way to handle IO when you have numerous connected sockets : you dn't have to create a specific thread for each connection, you can just use a few threads to do the same job.

我不知道NIO怎么实现同步和异步,我也不知道网上那么多Java NIO异步非阻塞的例子中,异步到底是怎么体现的,我想就阻塞和非阻塞来说。好了现在问题来了,Java的NIO中如何实现非阻塞的传输。简单的说,是利用选择器,一个线程可以查询一组socket,找出哪一个已经准备好读写的,然后按照顺序处理就绪的socket.。在这种情况下,I/O必须使用通道和缓冲区,而不是流。流和通道之间的关键区别在于流是基于字节的,而通道时基于块的。而对于网络程序性能的提高,一个很实用的方法就是开辟足够大的缓冲区。

但是,NIO 中的select到底是怎么轮询这些socket的,我们来看一个服务器端代码的实现:

package com.a2.nio.communication.charts;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * 之前的程序之所以不快,因为通道是操作块的,而我们每次只有一条记录
 * @author ChenHui
 *
 */
public class ChargenServer {
	public static void main(String[] args) throws IOException {
		ServerSocketChannel serverChannel = ServerSocketChannel.open();

		ServerSocket ss = serverChannel.socket();

		ss.bind(new InetSocketAddress(8082));

		serverChannel.configureBlocking(false);

		Selector selector = Selector.open();

		serverChannel.register(selector, SelectionKey.OP_ACCEPT);

		byte[] rotation = new byte[95 * 2];
		for (byte i = ' '; i <= '~'; i++) {
			rotation[i - ' '] = i;
			rotation[i + 95 - ' '] = i;
		}
		System.out.println("Server listening...");
		
		while (true) {

			selector.select();

			Set<SelectionKey> readyKey = selector.selectedKeys();
			Iterator<SelectionKey> iter = readyKey.iterator();
			while (iter.hasNext()) {
				SelectionKey key = iter.next();
				iter.remove();
				if (key.isAcceptable()) {
					ServerSocketChannel server = (ServerSocketChannel) key
							.channel();
					SocketChannel client = server.accept();
					System.out.println("Accept connection from " + client);
					client.configureBlocking(false);
					SelectionKey key2 = client.register(selector,
							SelectionKey.OP_WRITE);
					ByteBuffer buffer = ByteBuffer.allocate(74);
					buffer.put(rotation, 0, 72);
					buffer.put((byte) '\r');
					buffer.put((byte) '\n');
					buffer.flip();
					key2.attach(buffer);
				} else if (key.isWritable()) {
					SocketChannel client=(SocketChannel) key.channel();
					ByteBuffer buffer=(ByteBuffer) key.attachment();
					if(!buffer.hasRemaining()){
						//用下一行重新填充缓冲区
						buffer.rewind();
						int first=buffer.get();
						buffer.rewind();
						int position =first-' '+1;
						buffer.put(rotation, position, 72);
						buffer.put((byte)'\r');
						buffer.put((byte)'\n');
						buffer.flip();					
					}
					client.write(buffer);
					client.close();
				}
			}
		}

	}
}

上面代码中,通过select来轮询所有的连接,当有连接准备好的时候,马上执行:

ServerSocketChannel server = (ServerSocketChannel) key
							.channel();
					SocketChannel client = server.accept();

 

再建立一个channel来和客户端通信。其实代码很简单,我们再往里看selector

/**
     * Opens a selector.
     *
     * <p> The new selector is created by invoking the {@link
     * java.nio.channels.spi.SelectorProvider#openSelector openSelector} method
     * of the system-wide default {@link
     * java.nio.channels.spi.SelectorProvider} object.  </p>
     *
     * @return  A new selector
     *
     * @throws  IOException
     *          If an I/O error occurs
     */
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
}

 

在看provider里的代码:

public static SelectorProvider provider() {
        synchronized (lock) {
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            if (loadProviderFromProperty())
                                return provider;
                            if (loadProviderAsService())
                                return provider;
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }

 

每次只提供一个provider来创建selectorselector只做分发作用,而不做处理,,这样的方式比启线程池来处理这些连接来的快也能说通了。

类似于下面的reactor模式:

同步和异步,阻塞和非阻塞

当然再NIO的客户端中还有一点要注意:

SocketAddress address = new InetSocketAddress(HOST, PORT);
		SocketChannel client = SocketChannel.open();

		client.configureBlocking(false);
		
		client.connect(address);
		//
		client.finishConnect();
/********************************************/
		SocketAddress address = new InetSocketAddress(HOST, PORT);
		SocketChannel client = SocketChannel.open(address);
client.connect(address);
用上面的办法创建是非阻塞的,但是在程序实际使用连接之前必须调用finishConnect。原因是非阻塞通道连接之后会立即返回。而下面那种建立的是阻塞的连接。必须调用connect之后才会连接。

其实还是没有解决文章一开始的问题,而且客户端要不要用非阻塞的真的不好说。反正服务器端用是不会错的,那么FTP为什么可以传的很快,这我暂时也说不出个所以然来。但是前景还是会光明的。

同步和异步,阻塞和非阻塞

 

你可能感兴趣的:(同步和异步,阻塞和非阻塞)