《Java TCP/IP Socket 编程 》读书笔记之十二:各章节要点

  • Post author:
  • Post category:java





转载请注明出处:



http://blog.csdn.net/ns_code/article/details/16118955









T


CP/IPSocketsinjava读书笔记


第1章:简介


1、协议相当于相互通信的程序间达成的一种约定,它规定了分组报文的结构、交换方式、包含的意义以及怎样对报文所包含的信息进行解析。


2、TCP/IP协议族有IP协议、TCP协议和UDP协议。


3、TCP协议和UDP协议使用的地址叫做端口号,用来区分同一主机上的不同应用程序。TCP协议和UDP协议也叫端到端传输协议,因为他们将数据从一个应用程序传输到另一个应用程序,而IP协议只是将数据从一个主机传输到另一个主机。


4、在TCP/IP协议中,有两部分信息用来确定一个指定的程序:互联网地址和端口号:其中互联网地址由IP协议使用,而附加的端口地址信息则由传输协议(TCP或UDP协议)对其进行解析。


5、现在TCP/IP协议族中的主要socket类型为流套接字(使用TCP协议)和数据报套接字(使用UDP协议),其中通过数据报套接字,应用程序一次只能发送最长65507个字节长度的信息。


6、一个TCP/IP套接字由一个互联网地址,一个端对端协议(TCP协议或UDP协议)以及一个端口号唯一确定。


7、每个端口都标识了一台主机上的一个应用程序,实际上,一个端口确定了一个主机上的一个套接字。主机中的多个程序可以同时访问同一个套接字,在实际应用中,访问相同套接字的不同程序通常都属于一个应用(如web服务程序的多个副本),但从理论上讲,它们可以属于不同的应用。



第2章:基本套接字


1、编写TCP客户端程序,在实例化Socket类时,要注意,底层的TCP协议只能处理IP协议,如果传递的第一个参数是主机名字而不是你IP地址,Socket类具体实现的时候会将其解析成相应的地址,若因为某些原因连接失败,构造函数会抛出一个IOException异常。


2、TCP协议读写数据时,read()方法在没有可读数据时会阻塞等待,直到有新的数据可读。另外,TCP协议并不能确定在read()和write()方法中所发送信息的界限,接收或发送的数据可能被TCP协议分割成了多个部分。


3、编写TCP服务器端的程序将在accept()方法处阻塞,以等待客户端的连接请求,一旦取得连接,便要为每个客户端的连接建立一个Socket实例来进行数据通信。


4、在UDP程序中,创建DatagramPacket实例时,如果没有指定远程主机地址和端口,则该实例用来接收数据(尽管可以调用setXXX()等方法指定),如果指定了远程主机地址和端口,则该实例用来发送数据。


5、UDP程序在receive()方法处阻塞,直到收到一个数据报文或等待超时。由于UDP协议是不可靠协议,如果数据报在传输过程中发生丢失,那么程序将会一直阻塞在receive()方法处,这对客户端来说是肯定不行的,为了避免这个问题,我们在客户端使用DatagramSocket类的setSoTimeout()方法来制定receive()方法的最长阻塞时间,并指定重发数据报的次数,如果每次阻塞都超时,并且重发次数达到了设置的上限,则关闭客户端。


6、UDP服务器为所有通信使用同一套接字,这点与TCP服务器不同,TCP服务器则为每个成功返回的accept()方法创建一个新的套接字。


7、在UDP程序中,DatagramSocket的每一次receive()调用最多只能接收调用一次send()方法所发送的数据,而且,不同的receive()方法调用绝对不会返回同一个send()方法所发送的额数据。


8、在UDP套接字编程中,如果receive()方法在一个缓冲区大小为n的DatagramPscket实例中调用,而接受队列中的第一个消息长度大于n,则receive()方法只返回这条消息的前n个字节,超出的其他字节部分将自动被丢弃,而且也没有任何消息丢失的提示。因此,接受者应该提供一个足够大的缓存空间的DatagramPacket实例,以完整地存放调用receive()方法时应用程序协议所允许的最大长度的消息。一个DatagramPacket实例中所运行传输的最大数据量为65507个字节,即UDP数据报文所能负载的最多数据,因此,使用一个有65600字节左右缓存数组的数据总是安全的。


9、在UDP套接字编程中,每一个DatagramPacket实例都包含一个内部消息长度值,而该实例一接收到新消息,这个长度值便可能改变(以反映实际接收的消息的字节数)。如果一个应用程序使用同一个DatagramPacket实例多次调用receive()方法,每次调用前就必须显式地将消息的内部长度重置为缓冲区的实际长度。


10、另一个潜在问题的根源是DatagramPacket类的getData()方法,该方法总是返回缓冲区的原始大小,忽略了实际数据的内部偏移量和长度信息。



第3章:发送和接收数据


1、程序间达成的一种包含了信息交换的形式和意义的共识称为协议,用来实现特定应用程序的协议叫做应用程序协议。


2、TCP/IP协议的唯一约束是:信息必须在块中发送和接收,而块的长度必须是8的倍数,因此,我们可以认为TCP/IP协议中传输的信息是字节序列。


3、关于字符,对于每个整数值都比255小的一组字符,因为其每个字符都能够作为一个单独的字节进行编码,因此不需要其他信息,而对于可能使用超过一个字节的大整数的编码方式,就有多种方式对其进行编码,这就是编码方案。编码字符集和字符的编码方案结合起来称为字符集。在网络编程中,发送者和接收者必须在文本字符串的表示方式上达成共识,最简单的方法就是使用同一个标准字符集。


4、成帧技术解决了消息接收端如何定位消息的首尾位置的问题。与UDP协议不同,TCP协议中没有消息边界的概念,因此在使用TCP套接字时,成帧就是一个非常重要的考虑因素。


5、主要有两种技术能够使消息接收者准确地找到消息的结束位置:基于定界符和显式长度,前者对消息的结束由一个唯一的标记指出,即发送者在传输完数据后显式添加一个特殊字符序列,这个特殊标记不能在传输的数据中出现,当然,填充技术能够对消息中出现的界定符进行修改,从而使接受者不将其识别为界定符;后者在变长字段或消息前附加一个固定大小的字段值,用来指示该字段或消息中包含多少个字节。




第4章:进阶


1、主线程结束后,其他线程也可以继续执行,Java虚拟机只有在所有非守护线程都执行完毕的情况下才终止。


2、服务器一般每分钟都要执行上千次客户端的请求,因此,为了更好的分析异常,大部分的服务器都会将它们的活动记录写入日志,Java中可以用java.util.logging.Logger类来实现相关功能,在Java中,每个日志记录器由一个全局唯一的名字识别,可以通过气静态方法getLogger(stringname)来获取者由名字name标示的唯一记录器。默认情况下,每个logger有一个ConsoleHandler用来将消息打印到System.err中。Logger的一个重要特征是它是线程安全的,即可以在并行运行的不同线程中调用它的方法,而不需要在调用者中添加额外的同步措施,如果没有这个特征,由不同线程记录的不同消息将错乱无章地写入到日志中。


3、用线程池实现TCP服务器端时,首先创建一个ServerSocket实例,然后创建N个线程,每个线程反复循环,从(共享的)ServerSocket实例接收客户端连接。当多个线程同时调用一个ServerSocket实例的accept()方法时,它们都将阻塞等待,直到一个新的连接成功建立,然后系统选择一个线程,用于刚刚建立起的新的连接,其他线程则继续阻塞等待。如果在一个客户端连接被创建时,没有线程在accept()方法上阻塞(即所有的线程都在为其他连接服务),系统则将新的连接排列在一个队列中,直到下一次调用accept()方法。


4、利用线程池实现服务器端程序时,线程池的大小需要根据负载情况进行调整,以使客户端连接时间最短,理想的情况是有一个调度工具,可以在系统负载增加时扩展线程池的大小(低于上限值),负载减轻时缩减线程池的大小。Java中提供了Executor接口来管理调度线程,它就代表了一个根据某种策略来执行Runnable实例的对象其中可能包含了排队和调度等细节,或如何选择要执行的任务。在使用Executor时,任务是在Executor内部排队,而不是在网络系统中排队。


5、ExecutorService接口继承于Executor接口,当ExecutorService接口的实例调用execute()方法时,需要传入一个实现了Runnable接口的实例,如果必要,它将创建一个新的线程来处理任务,但它首先会尝试使用已有的线程,如果一个线程空闲了60秒以上,则将被移除线程池。值得注意的是,当达到稳定状态时,缓存线程池服务最终将保持合适的线程数,以使每个线程都保持忙碌,同时又很少创建或销毁线程。


6、阻塞式Socket编程中,Socket的I/O会因为多种原因而阻塞。数据输入方法read()和receive()在没有数据可读时会阻塞,TCP套接字的write()方法在没有足够的空间缓存传输的数据时可能阻塞,ServerSocket的accept()方法和Socket的构造函数都会阻塞等待。当调用一个已经阻塞的方法将使用应用程序停止,并使运行它的线程无效。


7、Write()方法调用会阻塞等待,直到最有一个字节成功写入到TCP实现的本地缓存中,如果可用的缓存空间比要写入的数据小,在write()方法调用返回前,必须把一些数据成功传输到连接的另一端。Java现在还没有提供任何使write()超时或有其他线程将其打断的方法,所以,如果一个可以在Socket实例上发送大量数据的协议可能会无限期地阻塞下去。


8、有两种类型的一对多服务:广播和多播。对于广播,(本地)网络中的所有主机都会接收到一份数据副本,对于多播,消息只是发送给一个多播地址,网络只是将数据分发给那些表示想要接收发送到该多播地址数据的主机。总的来说,只有UDP套接字允许广播和多播。IPv4的多播地址范围是224.0.0.0到239.255.255.255,IPv6中的多播地址是任何由FF开头的地址。除了少出系统暴露的多播地址外,发送者可以向异常范围内的任何地址发送数据。Java中多播应用程序主要通过MulticastSocket实例进行通信。


9、在TCPSocket通信中,其中一端的read()方法返回-1表明通信的另一端关闭了套接字,更确切地说,是关闭了套接字所关联的输出流。




第5章:NIO


1、NIO主要包括两个部分:java.nio.channles包介绍Selector和Channel抽象,java.nio包介绍Buffer抽象。Selector和Channel抽象的关键点是:一次轮询一组客户端,查找哪个客户端需要服务;Buffer则提供了比Stream抽象更高效和可预测的I/O。Channel使用的不是流,正是Buffer缓冲区来发送或读写数据。


2、Buffer抽象代表了一个有限容量的数据容器,其本质是一个数组,由指针指示了在哪存放数据和从哪读取数据。使用Buffer有两个主要的好处:第一,与读写缓冲区数据相关联的系统开销暴露给了程序员,可以由程序员直接控制操作;第二,一些对Java对象的特殊Buffer映射操作能够直接操作底层平台的资源。这些操作节省了在不同地址空间中复制数据的开销——这在现代计算机体系结构中是开销很大的操作。


3、NIO的强大功能部分来自于channel的非阻塞特性。NIO的Channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道。在非阻塞式信道上调用一个方法总是会返回。例如,在一个非阻塞式ServerSocketChannel上调用accept()方法,如果有连接请求在等待,则返回客户端SocketChannel,否则,返回null;read()方法在没有数据可读时,不会阻塞等待,而是返回0。在等待连接、读取数据等的时候,线程也可以做其他事情,这便实现了线程的异步操作


4、Selector类的select()方法会阻塞等待,直到有信道准备好了IO操作,或等待超时,或另一个线程唤醒了它(调用了该选择器的wakeup()方法)。select()方法返回的是自上次调用它之后,有多少通道变为就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。


5、我们在用Iterator迭代SelectionKey集合时,每次迭代末尾注意调用remove()方法。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除,以备下次该通道变成就绪时,Selector可以再次将其放入已选择键集中。如果不移除每个处理过的键,它就会在下次调用select()方法时仍然保留在集合中,而且可能会有无用的操作来调用它。Selector选择器实现了在单个线程中监听多个信道的功能。


6、缓冲区是定长的,不可以扩展容量,ByteBuffer是最常用的缓冲区。缓冲区中各索引值的大小关系:0=<mark=<position=<limit=<capacity。


7、allocateDirect()方法尝试分配直接缓存区,使用直接缓冲区,Java将从平台能够直接进行I/O操作的存储空间中为缓冲区分配后援存储空间,但不保证一定能成功,因此在尝试分配直接缓冲区后必须调用isDirect()方法进行检查,分配和销毁直接缓冲区通常要比分配和销毁非直接缓冲区消耗更多的系统资源。


8、Buffer的clear()方法并不改变缓冲区中的数据,它只是将position设置为0,并将limit设置为等于capacity,从而使缓冲区准备好从缓冲区的put操作或信道的读操作接收新的数据。flip()方法用来将缓冲区准备为数据传出状态,这通过将limit设置为position的当前值,再将position的值设为0来实现。Rewind()方法将position设置为0,并使mark值无效,limit值不变,这样便可以重复传送缓冲区中的数据。compact()方法将position与limit之间的元素复制到缓冲区的开始位置,从而为后续的read()/put()操作让出空间,但数据复制是一个非常耗费系统资源的操作,因此要保守地使用compact()方法。如果调用slice()方法创建了一个共享了原始缓冲区子序列的新缓冲区,则在先缓冲区上调用array()方法还是返回整个缓冲数组。


9、对于非阻塞SocketChannel来说,一旦已经调用connect()方法发起连接,底层套接字可能既不是已经连接,也不是没有连接,而是正在连接。由于底层协议的工作机制,套接字可能会在这个状态一直保持下去,这时候就需要循环地调用finishConnect()方法来检查是否完成连接,在等待连接的同时,线程也可以做其他事情,这便实现了线程的异步操作。


10、每个选择器都有一组与之关联的信道,一个信道也可以注册多个Selector实例,因此可以有多个关联的SelectionKey实例。任何对key所关联的兴趣操作集的改变,都只在下次调用select()方法后才会生效。对于serverSocketChannel来说,accept是唯一的有效操作,而对于socketChannel来说,有效操作包括读、写和连接,对于DatagramChannle,只有读写操作是有效的。一个信道可能只与一个选择器注册一次,因此后续对register()方法的调用只是简单地更新该key所关联的兴趣操作集。


11、selectedKeys()方法返回的键集是可修改的,实际上在两次调用select()方法之间,都必须手动将其清空,换句话说,select()方法只会在已有的所选键集上添加键,它们不会创建新的建集。




第6章:深入剖析


详见博客。。。