客户端socket请求连接Serversocket的请求连接,按照请求顺序进入客户端连接请求队列(队列的容量是由操作系统完成的),ServerSocket的构造函数中的backlog就是用来指定请求队列的长度。 这个值会失效的三种情况:大于操作系统默认值|小于等于0|没有设置。 (见下面)
serversocket中的accept方法,会从客户端连接请求队列(先进先出)中取出一个请求连接的请求,生成一个用于通信的socket。只有当serversocket的accept方法成功返回时,才表明客户端与服务端建立了连接。
socket连接是Java中进行通信的基本方式,也是效率最高的方式,虽然他有http等让是进行http请求,但是如果是进行tcp、下载等通信,还是使用socket更好。Java中封装了非常完美的socket机制,使用也非常简单。主要包括socket和serversocket。
socket的使用非常简单,主要包括的构造方法有:socket(),socket(string host,string port),socket(Inetaddress address,int port)等,非常明白了,通过传入host和port进行socket的请求,当在创建相应的套接字实例的时候,会自动去对相应的ip和port进行连接,只有当连接成功,才表示相应的套接字建立成功,才可以进行相应的I/O操作。通过getOutputStream和getInputStream获取相应的输入输出流,进行相应的I/O操作,但是有一个是比较特别的,getChannel用来获取SocketChannel,他之所以特别是因为他属于java.nio.channels下面的类,其继承于java.nio.channels.SelectableChannel,就是说在进行nio非阻塞式的请求连接时,他非常有用,具体参见http://www.cnblogs.com/likwo/archive/2010/06/29/1767814.html。可能有些人会问,对于可以通过设置服务器连接的timeout来防止过多的阻塞,但是如果对于超过timeout,socket一般是抛出超时异常,这样就算对异常进行了处理,也将会从新建立socket连接,浪费消费 重建的资源。例如QQ聊天,当你打开一个聊天面板,很久不说话的时候,并不会自动为你断开socket连接,而是一直处于阻塞状态,直到你发送了新的信息,再进行处理,因此nio的阻塞方式会更好些。
对于serversocket也比较简单,常用的只有四个构造函数:
l ServerSocket()throws IOException
l ServerSocket(int port) throws IOException
l ServerSocket(int port, int backlog) throws IOException
l ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
分别对这几个构造进行简单的解释:
第一个无参构造,只是创建一个serversocket实例,但是不进行任何端口的监听,你还必须通过bind方法进行端口的绑定,好处就是在绑定之前可以进行相应属性的设置,例如so_reuseaddress等;
第二个构造函数需要一个端口(1024-65535),一般不使用0-1023之间的,这个属于系统占用的预留端口,但是如果你传入的端口号为0,则会默认使用匿名端口,就是系统随机分配一个端口,进行暂时通信,这个匿名方式,一般情况下不使用;
第三个构造函数需要一个端口,和一个backlog(监听对列大小),serversocket进行某端口的监听,当有多个连接请求是,每个请求默认都会放入一个请求队列里,如果你没有设置这个值,则默认为操作系统的值,根据不同的系统有所不同,例如40等,有几种情况,这个值将会失效:大于操作系统默认值|小于等于0|没有设置。如果设置了,而没有及时对队列进行处理,则会报ConnectException异常;
第四个构造函数除了具有端口、队列大小外,还具有一个参数是ip地址,就是进行相应ip地址的绑定。当然,这个进行的是服务器ip地址的绑定,不会限制客户端的ip访问。当一台服务器存在多个网卡的时候,就需要通过这个参数来设置客户端访问的ip。
服务器socket的关闭,通过使用close进行关闭,使用isclosed进行判断,还可以进行端口的绑定判断。
对于服务器close方法,需要有一点进行说明:调用close方法之后,操作系统并不会立即进行端口的释放,依旧会对旧端口占用一段时间,以防止客户端发送的数据有延迟现象。因此有时候,就算你进行了close方法的调用,进行了端口的释放,但是如果你立即进行同一个端口的连接时,依旧会包端口占用异常,这个是可以理解的。
serversocket通过使用accept方法进行客户端请求的处理,每当请求队列里有客户端请求时,serversocket就会从队列顶端取一个socket请求进行处理,生成一个socket来负责与客户端通信。如果一个时间只能处理一个socket,当有多个客户端请求时,则必须要排队处理,等待所有前面的socket处理完,这是个极其痛苦,并且不合理的过程。因此,引入了线程的概念。
为了实现客户端请求的快速相应和快速处理,据是高并发,则必须使用多线程机制。主题思想是:serversocket通过accept建立一个socket,然后起一个线程,把这个socket扔给新建的线程进行处理,而serversocket所在的主线程,则继续去监听端口,以此实现多线程通信。一般有三种方式:
上面的方式非常简单,能够处理基本的多线程问题,当数据量不大时,应该没有什么问题,但是如果数据量过大时,就会出现严重的性能,甚至是宕机问题。其缺点主要有如下几个:
a:每个socket请求,建立一个连接,当每个都是进行简短的通信时,则异常的耗费系统建立、销毁线程资源。
b:如果建立线程太多,每个线程都会占用一定的系统内存,这样将导致内存溢出。
c:频繁地对线程进行建立 销毁,会导致操作系统进行频繁的cpu切换线程切换,这样也会非常耗费系统资源。
自己写线程池,能够对线程池的工作原理以及工作情况,更加的了解和控制,但是由于线程池必然涉及到多线程问题,因此为了防止出现死锁、线程泄漏、并发错误、任务过载等问题,需要性能非常好的机制,一般不推荐个人现实。如果非要实现,可以通过使用linkedlist
这个jar包都是一些并发编程会经常使用到的工具类,主要有阻塞队列,原子操作的map以及线程池等。其基本包括Executor、ExecutorService接口和Executors类,两个接口定义了执行线程的方法,而Executors则定义了管理线程池的方法,主要可以创建的常用线程池有:
newSingleThreadScheduledExecutor()
创建一个可以延迟执行和定时执行的单线程线程池
newSingleThreadExecutor()
创建一个运行单线程的线程池 new LinkedBlockingQueue
newScheduledThreadPool(int corePoolSize)
创建一个可以延迟执行和定时执行的线程池,设定线程数,常用来代替Timer(定时器)
newFixedThreadPool(int nThreads)
创建一个固定线程数的线程池 new LinkedBlockingQueue
newCachedThreadPool()
创建一个带有缓冲区的线程池 new SynchronousQueue
然后通过生成的线程池的execute方法进行线程的执行,具体可以百度下哈哈
使用线程池有以下几点风险:
1、死锁。任何多线程都不可避免的问题。但是对于线程池可能会存在另一种死锁:就是线程池中的线程都在等待一个资源,而这个资源需要执行A后得到,而由于线程池没有可用的线程,导致A无法执行,故而也会发生死锁。
2、系统资源不足。多线程必定需要大量的内存资源,可能出现内存泄漏问题。
3、并发错误。
4、线程泄漏。就是所有的线程池中的线程都在等待输入资源,或者都抛出了异常而没有捕获,则会导致线程池中所有的线程假死。
5、线程过载。运行线程过多,导致过载,这个可以通过设置线程池的大小来进行一定成功的避免。
至于如何避免,主要是要在使用多线程时要小心,同时不要使用destroy despause 等操作,尽量使用sleep notify wait等操作,在这里不详细说明了。
在多线程大师Doug Lea的贡献下,在JDK1.5中加入了许多对并发特性的支持,例如:线程池。这里介绍的就是1.5种的线程池的简单使用方法。
一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。
当一个任务通过execute(Runnable)方法欲添加到线程池时:
也就是:处理任务的优先级为:
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
unit可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:
NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。
workQueue我常用的是:java.util.concurrent.ArrayBlockingQueue
handler有四个选择:
对这两段程序的说明:
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
Socket 通信示例
主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接,而建立 Socket 连接必须需要底层 TCP/IP 协议来建立 TCP 连接。建立 TCP 连接需要底层 IP 协议来寻址网络中的主机。我们知道网络层使用的 IP 协议可以帮助我们根据 IP 地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过 TCP 或 UPD 的地址也就是端口号来指定。这样就可以通过一个 Socket 实例唯一代表一个主机上的一个应用程序的通信链路了。
首先看两个概念: 短连接--长连接
send()函数返回了实际发送的长度,在网络不断的情况下,它绝不会返回(发送失败的)错误,最多就是返回0。对于TCP你可以字节写一个循环发送。当send函数返回SOCKET_ERROR时,才标志着有错误。但对于UDP,你不要写循环发送,否则将给你的接收带来极大的麻烦。所以UDP需要用SetSockOpt来改变Socket内部Buffer的大小,以能容纳你的发包。明确一点,TCP作为流,发包是不会整包到达的,而是源源不断的到,那接收方就必须组包。而UDP作为消息或数据报,它一定是整包到达接收方。
2、关于接收,一般的发包都有包边界,首要的就是你这个包的长度要让接收方知道,于是就有个包头信息,对于TCP,接收方先收这个包头信息,然后再收包数据。一次收齐整个包也可以,可要对结果是否收齐进行验证。这也就完成了组包过程。UDP,那你只能整包接收了。要是你提供的接收Buffer过小,TCP将返回实际接收的长度,余下的还可以收,而UDP不同的是,余下的数据被丢弃并返回WSAEMSGSIZE错误。注意TCP,要是你提供的Buffer佷大,那么可能收到的就是多个发包,你必须分离它们,还有就是当Buffer太小,而一次收不完Socket内部的数据,那么Socket接收事件(OnReceive),可能不会再触发,使用事件方式进行接收时,密切注意这点。这些特性就是体现了流和数据包的区别。参考:http://www.cnblogs.com/canghaitianyuan/archive/2012/11/16/2772987.html
http://my.oschina.net/ksfzhaohui/blog/95451
《 java并发编程实践》
《java网络编程精解》
http://dalezhu.javaeye.com/blog/186895