java socket用法(一)

1.ServerSocket;

ServerSocket有以下3 个选项。
l  SO_TIMEOUT:表示等待客户连接的超时时间。
l  SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。
l  SO_RCVBUF:表示接收数据的缓冲区的大小。

SO_TIMEOUT选项
l 设置该选项:public void setSoTimeout(int timeout) throws SocketException
l 读取该选项:public int getSoTimeout () throws IOException
SO_TIMEOUT 表示ServerSocket 的accept()方法等待客户连接的超时时间,以毫
秒为单位。如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的
默认值。
当服务器执行ServerSocket的accept()方法时,如果连接请求队列为空,服务器就
会一直等待,直到接收到了客户连接才从accept()方法返回。如果设定了超时时间,那
么当服务器等待的时间超过了超时时间,就会抛出SocketTimeoutException,它是
InterruptedException的子类。

example:

import java.io.*;
import java.net.*;
public class TimeoutTester{
public static void main(String args[])throws IOException{
ServerSocket serverSocket=new ServerSocket(8000);
serverSocket.setSoTimeout(6000); //等待客户连接的时间不超过6 秒
Socket socket=serverSocket.accept();
socket.close();
System.out.println("服务器关闭");
}
}

 运行6秒后会抛出异常:

C:\chapter03\classes>java TimeoutTester
Exception in thread "main" java.net.SocketTimeoutException: Accept timed out
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.PlainSocketImpl.accept(Unknown Source)
at java.net.ServerSocket.implAccept(Unknown Source)
at java.net.ServerSocket.accept(Unknown Source)
at TimeoutTester.main(TimeoutTester.java:8)

如果把程序中的“serverSocket.setSoTimeout(6000)”注释掉,那么serverSocket.
accept()方法永远不会超时,它会一直等待下去,直到接收到了客户的连接,才会从
accept()方法返回。

 

 

2.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

在以上构造方法中,参数port 指定服务器要绑定的端口(服务器要监听的端口),
参数backlog指定客户连接请求队列的长度,参数bindAddr指定服务器要绑定的IP地址。

 

import java.net.*;
public class Client {
public static void main(String args[])throws Exception{
final int length=100;
String host="localhost";
int port=8000;
Socket[] sockets=new Socket[length];
for(int i=0;i<length;i++){ //试图建立100 次连接
sockets[i]=new Socket(host, port);
System.out.println("第"+(i+1)+"次连接成功");
}
Thread.sleep(3000);
for(int i=0;i<length;i++){
sockets[i].close(); //断开连接
}
}
}


import java.io.*;
import java.net.*;
public class Server {
private int port=8000;
private ServerSocket serverSocket;
public Server() throws IOException {
serverSocket = new ServerSocket(port,3); //连接请求队列的长度为3
System.out.println("服务器启动");
}
public void service() {
while (true) {
Socket socket=null;
try {
socket = serverSocket.accept(); //从连接请求队列中取出一个连接
System.out.println("New connection accepted " +
socket.getInetAddress() + ":" +socket.getPort());
}catch (IOException e) {
e.printStackTrace();
}finally {
try{
if(socket!=null)socket.close();
}catch (IOException e) {e.printStackTrace();}
}
}
}
public static void main(String args[])throws Exception {
Server server=new Server();
Thread.sleep(60000*10); //睡眠10 分钟
//server.service();
}
}

 

Client 试图与Server 进行100 次连接。在Server 类中,把连接请求队列的长度设
为3。这意味着当队列中有了3 个连接请求时,如果Client 再请求连接,就会被Server
拒绝

 

结是如下:

第1 次连接成功
第2 次连接成功
第3 次连接成功
Exception in thread "main" java.net.ConnectException: Connection refused: connect
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.PlainSocketImpl.doConnect(Unknown Source)
at java.net.PlainSocketImpl.connectToAddress(Unknown Source)

at java.net.PlainSocketImpl.connect(Unknown Source)
at java.net.SocksSocketImpl.connect(Unknown Source)
at java.net.Socket.connect(Unknown Source)
at java.net.Socket.connect(Unknown Source)
at java.net.Socket.<init>(Unknown Source)
at java.net.Socket.<init>(Unknown Source)
at Client.main(Client.java:10)

从以上打印结果可以看出,Client 与Server 在成功地建立了3 个连接后,就无法
再创建其余的连接了,因为服务器的队列已经满了。

 

如果是改为

  public static void main(String args[])throws Exception {
Server server=new Server();
//Thread.sleep(60000*10); //睡眠10 分钟
server.service();
}

  2.1 设定绑定的IP地址

  如果主机只有一个IP 地址,那么默认情况下,服务器程序就与该IP 地址绑定。
ServerSocket的第4 个构造方法ServerSocket(int port, int backlog, InetAddress bindAddr)
有一个bindAddr 参数,它显式指定服务器要绑定的IP地址,该构造方法适用于具有多
个IP地址的主机。假定一个主机有两个网卡,一个网卡用于连接到Internet, IP地址
为222.67.5.94,还有一个网卡用于连接到本地局域网,IP 地址为192.168.3.4。如果服
务器仅仅被本地局域网中的客户访问,那么可以按如下方式创建ServerSocket:
ServerSocket serverSocket=new ServerSocket(8000,10,InetAddress.getByName ("192.168.3.4"));

 

 2.2默认构造方法的作用

ServerSocket有一个不带参数的默认构造方法。通过该方法创建的ServerSocket不
与任何端口绑定,接下来还需要通过bind()方法与特定端口绑定。
这个默认构造方法的用途是,允许服务器在绑定到特定端口之前,先设置
ServerSocket的一些选项。因为一旦服务器与特定端口绑定,有些选项就不能再改变了。
在以下代码中,先把ServerSocket 的SO_REUSEADDR 选项设为true,然后再把

 

它与8000 端口绑定:
ServerSocket serverSocket=new ServerSocket();
serverSocket.setReuseAddress(true); //设置ServerSocket 的选项
serverSocket.bind(new InetSocketAddress(8000)); //与8000 端口绑定
如果把以上程序代码改为:
ServerSocket serverSocket=new ServerSocket(8000);
serverSocket.setReuseAddress(true); //设置ServerSocket 的选项
那么serverSocket.setReuseAddress(true)方法就不起任何作用了,因为SO_
REUSEADDR选项必须在服务器绑定端口之前设置才有效。

 

 

 

打印结果如下:

第1 次连接成功
第2 次连接成功
第3 次连接成功

第100 次连接成功

 

 

3. setResuseAddress(boolean b) 的用法

这个选项与Socket的SO_REUSEADDR选项相同,用于决定如果网络上仍然有数
据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到与旧的ServerSocket
同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,
允许重用端口,而在某些操作系统中不允许重用端口。
当ServerSocket 关闭时,如果网络上还有发送到这个ServerSocket 的数据,这个
ServerSocket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送
过来的延迟数据,然后再释放端口。
许多服务器程序都使用固定的端口。当服务器程序关闭后,有可能它的端口还会
被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占

用,使得服务器程序无法绑定到该端口,服务器启动失败,并抛出BindException:
Exception in thread "main" java.net.BindException: Address already in use: JVM_Bind
为了确保一个进程关闭了ServerSocket 后,即使操作系统还没释放端口,同一个
主机上的其他进程还可以立刻重用该端口,可以调用ServerSocket 的setResuse-
Address(true)方法:
if(!serverSocket.getResuseAddress())serverSocket.setResuseAddress(true);
值得注意的是,serverSocket.setResuseAddress(true)方法必须在ServerSocket还没有
绑定到一个本地端口之前调用,否则执行serverSocket.setResuseAddress(true)方法无效。
此外,两个共用同一个端口的进程必须都调用serverSocket.setResuseAddress(true)方法,
才能使得一个进程关闭ServerSocket后,另一个进程的ServerSocket还能够立刻重用相
同端口。

 

4.SO_RCVBUF选项

 

SO_RCVBUF表示服务器端的用于接收数据的缓冲区的大小,以字节为单位。一般
说来,传输大的连续的数据块(基于HTTP或FTP协议的数据传输)可以使用较大的缓
冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信
(Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。

 

ServerSocket serverSocket=new ServerSocket();
int size=serverSocket.getReceiveBufferSize();
if(size<131072) serverSocket.setReceiveBufferSize(131072); //把缓冲区的大小设为128K
serverSocket.bind(new InetSocketAddress(8000)); //与8 000 端口绑定

 

 

 5.创建线程池

 


l 除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。每个线程本
身都会占用一定的内存(每个线程需要大约1M 内存),如果同时有大量客户
连接服务器,就必须创建大量工作线程,它们消耗了大量内存,可能会导致
系统的内存空间不足。
l 如果线程数目固定,并且每个线程都有很长的生命周期,那么线程切换也是
相对固定的。不同操作系统有不同的切换周期,一般在20 毫秒左右。这里所
说的线程切换是指在Java 虚拟机,以及底层操作系统的调度下,线程之间转
让CPU的使用权。如果频繁创建和销毁线程,那么将导致频繁地切换线程,
因为一个线程被销毁后,必然要把CPU转让给另一个已经就绪的线程,使该
线程获得运行机会。在这种情况下,线程之间的切换不再遵循系统的固定切
换周期,切换线程的开销甚至比创建及销毁线程的开销还大。

 采用线程池:预先创建了一些工作线程,它们不断从工作队列中取出任务,然后执行该任务。当工
作线程执行完一个任务时,就会继续执行工作队列中的下一个任务。线程池具有以下
优点:
l 减少了创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行多
个任务。
l 可以根据系统的承载能力,方便地调整线程池中线程的数目,防止因为消耗
过量系统资源而导致系统崩溃。

 

package multithread2;
import java.util.LinkedList;
public class ThreadPool extends ThreadGroup {
private boolean isClosed=false; //线程池是否关闭
private LinkedList<Runnable> workQueue; //表示工作队列
private static int threadPoolID; //表示线程池ID
private int threadID; //表示工作线程ID
public ThreadPool(int poolSize) { //poolSize 指定线程池中的工作线程数目
super("ThreadPool-" + (threadPoolID++));
setDaemon(true);
workQueue = new LinkedList<Runnable>(); //创建工作队列
for (int i=0; i<poolSize; i++)
new WorkThread().start(); //创建并启动工作线程
}
/** 向工作队列中加入一个新任务,由工作线程去执行该任务 */
public synchronized void execute(Runnable task) {
if (isClosed) { //线程池被关则抛出IllegalStateException异常
throw new IllegalStateException();
}
if (task != null) {
workQueue.add(task);
notify(); //唤醒正在getTask()方法中等待任务的工作线程
}
}

/** 从工作队列中取出一个任务,工作线程会调用此方法 */
protected synchronized Runnable getTask()throws InterruptedException{
while (workQueue.size() == 0) {
if (isClosed) return null;
wait(); //如果工作队列中没有任务,就等待任务
}
return workQueue.removeFirst();
}
/** 关闭线程池 */
public synchronized void close() {
if (!isClosed) {
isClosed = true;
workQueue.clear(); //清空工作队列
interrupt(); //中断所有的工作线程,该方法继承自ThreadGroup类
}
}
/** 等待工作线程把所有任务执行完 */
public void join() {
synchronized (this) {
isClosed = true;
notifyAll(); //唤醒还在getTask()方法中等待任务的工作线程
}
Thread[] threads = new Thread[activeCount()];
//enumerate()方法继承自ThreadGroup类,获得线程组中当前所有活着的工作线程
int count = enumerate(threads);
for (int i=0; i<count; i++) { //等待所有工作线程运行结束
try {
threads[i].join(); //等待工作线程运行结束
}catch(InterruptedException ex) { }
}
}
/** 内部类:工作线程 */
private class WorkThread extends Thread {
public WorkThread() {
//加入到当前ThreadPool 线程组中
super(ThreadPool.this,"WorkThread-" + (threadID++));
}
public void run() {
while (!isInterrupted()) { //isInterrupted()方法继承自Thread 类,判断线程是否被中断
Runnable task = null;
try { //取出任务
task = getTask();
}catch (InterruptedException ex){}
// 如果getTask()返回null 或者线程执行getTask()时被中断,则结束此线程
if (task == null) return;
try { //运行任务,异常在catch代码块中捕获
task.run();
} catch (Throwable t) {
t.printStackTrace();
}
} //#while
} //#run()
} //#WorkThread 类
}

 

在ThreadPool类中定义了一个LinkedList类型的workQueue成员变量,它表示工
作队列,用来存放线程池要执行的任务,每个任务都是Runnable实例。ThreadPool 类
的客户程序(利用ThreadPool 来执行任务的程序)只要调用ThreadPool 类的execute
(Runnable task)方法,就能向线程池提交任务。在ThreadPool类的execute()方法中,先
判断线程池是否已经关闭。如果线程池已经关闭,就不再接收任务,否则就把任务加

入到工作队列中,并且唤醒正在等待任务的工作线程。
在ThreadPool 类的构造方法中,会创建并启动若干工作线程,工作线程的数目由
构造方法的参数poolSize决定。WorkThread类表示工作线程,它是ThreadPool类的内
部类。工作线程从工作队列中取出一个任务,接着执行该任务,然后再从工作队列中
取出下一个任务并执行它,如此反复。
工作线程从工作队列中取任务的操作是由ThreadPool 类的getTask()方法实现的,
它的处理逻辑如下:
l 如果队列为空并且线程池已关闭,那就返回null,表示已经没有任务可以执
行了;
l 如果队列为空并且线程池没有关闭,那就在此等待,直到其他线程将其唤醒
或者中断;
l 如果队列中有任务,就取出第一个任务并将其返回。
线程池的join()和close()方法都可用来关闭线程池。join()方法确保在关闭线程池之
前,工作线程把队列中的所有任务都执行完。而close()方法则立即清空队列,并且中
断所有的工作线程。
ThreadPool 类是ThreadGroup类的子类。ThreadGroup 类表示线程组,它提供了一
些管理线程组中线程的方法。例如,interrupt()方法相当于调用线程组中所有活着的线
程的interrupt()方法。线程池中的所有工作线程都加入到当前ThreadPool 对象表示的线
程组中。ThreadPool类在close()方法中调用了interrupt()方法:

/** 关闭线程池 */
public synchronized void close() {
if (!isClosed) {
isClosed = true;
workQueue.clear(); //清空工作队列
interrupt(); //中断所有的工作线程,该方法继承自ThreadGroup类
}
}

以上interrupt()方法用于中断所有的工作线程。interrupt()方法会对工作线程造成以
下影响:
l 如果此时一个工作线程正在ThreadPool 的getTask()方法中因为执行wait()方
法而阻塞,则会抛出InterruptedException;
l 如果此时一个工作线程正在执行一个任务,并且这个任务不会被阻塞,那么
这个工作线程会正常执行完任务,但是在执行下一轮while (!isInterrupted())
{…}循环时,由于isInterrupted()方法返回true,因此退出while循环。
如例程3-7所示,ThreadPoolTester 类用于测试ThreadPool的用法。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值