您的位置:首页 > 理论基础 > 计算机网络

Java网络编程精解学习笔记(第三章 serverSocket详解)

2017-01-06 20:07 337 查看
1.构造ServerSocket
ServerSocket的构造方法有以下几种重载方法:
ServerSocket()
ServerSocket(int port)
ServerSocket(int port, int backlog)
ServerSocket(int port, int backlog, InetAddress bindAddr)
参数port表示端口,backlog表示请求队列,IentAddress表示绑定的服务器地址。
a>绑定端口:
除了第一个,其他构造方法都会绑定一个端口,例如:
ServerSocket ss = new ServerSocket(80);
如果绑定失败,则会抛出IOException。更确切来说是BindException,BindException是IOException的子类。绑定失败一般有两个原因:1.该端口已经别某个端口占用。2.用户不是超级管理员,不能绑定1-1023端口。如果端口指定的是0,则系统会默认分配给ServerSocket一个未被占用的匿名端口。由于客户端与服务器端连接需要知道服务器端的端口,所有不建议使用匿名端口。
b>设定客户端连接请求的长度:
当服务器进程运行时,会同时监听多个客户端连接请求。客户端的连接请求是由操作系统管理的,操作系统会把连接请求放入到一个先进先出的队列里,一般操作系统会规定队列的长度(一般是50)。当请求到达队列上限后其他请求就会被拒绝掉。只有当服务器执行serverSocket.accept(),把请求取出后,队列里才能腾出空位加入新的请求。
对于客户端来说,发出的连接请求加入到服务器队列则说明连接成功,客户端的socket构造方法返回。如果连接请求被拒绝,则会抛出ConnectionException。
backlog参数用于显示的指定队列的长度,指定的长度会覆盖操作系统指定的长度。但是有几种情况会依然采用操作系统指定的最大长度:1.backlog的长度大于操作系统指定的长度。2.backlog的长度小于等于0。 3.没有指定backlog参数。
以下例程用于演示连接请求参数:
Client.java:
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException, InterruptedException {
String host = "localhost";
int port = 8000;

Socket[] sockets = new Socket[10];
for(int i=0; i<10; i++) {
sockets[i] = new Socket(host, port);
System.out.println("第"+i+"次连接建立成功");
}

Thread.sleep(3000);
for(Socket s : sockets) {
s.close();
}
}
}
Server.java:
public class Server {
ServerSocket ss = null;

public Server() throws IOException {
ss = new ServerSocket(8000, 3);
System.out.println("服务器启动成功");
}

public void server() throws IOException {
while(true) {
Socket s = null;
try{
s = ss.accept();
System.out.println(s.getInetAddress() + ":" + s.getPort());
}finally {
if(s != null) {
s.close();
}
}
}
}

public static void main(String[] args) throws IOException, InterruptedException {
Server s = new Server();
//Thread.sleep(60000);
s.server();
}
}
如果把s.server()注释掉,由于服务器无法执行serverSocket.accept(),连接请求不被取出,所以请求连接3次后,其他的请求都被拒绝掉。如果打开s.server()。队列中的请求不断被消费,所以所有请求都会成功建立连接。
c>设定绑定的IP地址:
如果服务器只有一个Ip,那么ServerSocket会默认绑定这个IP。如果服务器有多个IP,则可以通过bindAddr这个参数指定绑定哪个IP地址。
d>默认构造方法的作用:
不带参数的构造方法没有绑定任何端口。该构造方法的作用是在绑定端口之前设置ServerSocket的某些属性。因为一旦服务器绑定端口后,某些属性就不能修改了。例如SO_REUSEADDR。
ServerSocket ss = new ServerSocket();
ss.setReuseAddress(true);
ss.bind(new InetSocketAddress(8000));
如果写成:
ServerSocket ss = new ServerSocket(8000);
ss.setReuseAddress(true);则该属性不会生效。

2.接收和关闭与客户的连接:
ServerSocket的accept()方法会从队列中取出一个连接请求,建立一个与客户端连接的Socket对象并且返回。如果队列中没有连接请求,accept()方法就会一直等下去。
接下来服务器从socket对象中获取输入,输出流。与客户端进行数据通信。当服务器端向客户端发送数据时,客户端断开了连接,那么会抛出一个IOException的子类SocketException。这只是服务器与单个客户端通信的连接,该异常应该被捕获,使得不能中断服务器与其他客户端的通信。
以下是单线程服务器采用的通信流程:
public class CloseSocket {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8000);

while(true) {
Socket s = null;
try {
s = ss.accept();
System.out.println(s.getInetAddress() + ":" + s.getPort());
} catch (IOException e) {
e.printStackTrace();
}finally {
if(s != null) {
s.close();
}
}
}
}

3.关闭ServerSocket:
ServerSocket()调用close()方法会释放占用的端口,断开所有与客户端的连接。如果不显示调用close()方法,当服务器进程结束后系统也会调用close()。所以没有必要再进程结束前调用close()。
在某些情况下,为了及时释放端口,以便让其他进程绑定该端口,可以显示调用close()方法。
serverSocket的isClose()是判断是否调用了close()方法。只有调用了isClose()才返回true。否则即使ServerSocket没有与端口绑定,该方法也返回false。
ServerSocket的isBound()方法判断是否与端口进行了绑定。如果ServerSocket已经与一个端口绑定过,及时它已经被关闭。isBound也返回true。
判断ServerSocket绑定一个端口并且没有被关闭的方法:
serverSocket.isBound() && !serverSocket.isClose();

4.获取ServerSocket的信息:
public InetAddress getInetAddress() //获取服务端IP地址、
public int getLocalPort() //获取服务端端口
构造方法中Port为0时,操作系统就会为ServerSocket分配一个匿名端口,通过getLocalPort就可以获取该端口,例程如下:
public class SocketTest {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(0);
System.out.println(ss.getLocalPort());
}
}
多数的服务器使用固定端口,方便客户端的连接。匿名端口使用与服务器端与客户端的临时通信。通信结束连接断开,并且ServerSocket占用的端口也释放。
FTP用于本地与远程服务器传输文件,它的一种方式使用了匿名端口。FTP使用两条并行的TCP连接,一条控制连接,一条数据连接。控制连接用于传输用户名,密码,修改服务器目录命令,上传下载命令等。数据连接用于上传下载文件用。服务器的控制连接监听在21端口。
数据连接的创建有两种 方式:
1>服务器的数据连接监听在20端口上,客户端主动请求连接到服务器端。

2>客户端首先创建一个监听在匿名端口的serverSocket。然后把这个端口号(通过getLocalPort获取)发送给服务器端。服务器端主动请求连接到客户端。该种方法就使用了serverSocket的匿名端口用于传输临时数据。

5.ServerSocket选项:
a>SO_TIMEOUT
设置该选项:public void setSoTimeout(int timeout)
获取该选项:public int getSoTimeout()
服务器端等待客户端连接的超时时间。服务器端的accept()方法会从连接队列中取请求连接,如果队列为空服务器就会一直等下去,直到到达设定的超时时间为止。如果超时时间设置的是0,表示一直会等待下去,这是ServerSocket的默认值,单位是毫秒。
如果服务器端超出了超时时间就会抛出SocketTimeoutException,它是InterruptedException的子类。例程如下:
public class SocketTimeout {、
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8000);
ss.setSoTimeout(5000);
Socket s = ss.accept();
s.close();
}
}
b>SO_REUSEADDR选项:
设置该选项:public void setReuseAddress(boolean b)
获取该选项: public boolean getReuseAddress()
该参数表示网络上还有数据没有传送到ServerSocket时,是否允许新的serverSocket绑定到老的ServerSocket上。默认值取决于操作系统,有的操作系统运行,有的不允许。
当网络上还有数据没有发送到ServerSocket上时,当关闭它时先不释放绑定的端口,数据全部接收完后在释放。许多服务器都使用固定端口,当服务器停止后,它所绑定的端口不会立即释放,如果此时重启服务器,由于该端口被占用导致绑定失败,抛出BindException。为了确保服务器停止后,新的进程能立即绑定该端口,可以使用setReuseAddress(true)方法。值得注意的是:该选项在在ServerSocket绑定端口前设置才有效。而且新的和老的ServerSocket同时设置才会起作用。
c>SO_RCVBUF选项:
设置该选项:public void setReceiveBufferSize(int size)
获取该选项:public int getReceiveBufferSize()
该选项用于设置服务器接收数据时的缓冲区大小,单位是字节。如果传输的数据量比较大,则将该选项值设置大些,以减少传输次数提高效率。如果传输的数据量比较少,并且传输较频繁则该选项设置小些。确保及时把数据传送给服务器。该选项的默认值与操作系统有关。
无论在绑定端口前还是绑定端口后设置该选项都有效,但是特殊情况是:当设置的缓冲区大于64K时,需要在绑定端口前设置该选项:

ServerSocket ss = new ServerSocket();
int size = ss.getReceiveBufferSize();
if(size < 131072) ss.setReceiveBufferSize(131072); //设置缓冲区大小为128K
ss.bind(new InetSocketAddress(8000));
执行setReceiveBufferSize选项后, serverSocket.accept()获取的所有Socket对象的缓冲区大小都为设置的值。
d>设置连接时间,带宽,延时的重要性
、设置该选项:public void setPerformancePreferences(int connectionTime, int latency, int bandwidth); 该选项的作用与Socket相同,用于设置连接时间,延时和带宽的相对重要性。

6.创建多线程服务器:
服务器为客户端提供的连接数越多,响应客户端的速度越快,表示服务器的并发性能越高。本节提供了3种并发方法:1.为每个客户端生成一个服务线程。2.使用线程池,由其中的工作线程为客户服务。3.使用JDK提供的线程池。
客户端:
public class Client {
public static void main(String[] args) {
Socket socket = null;
try {
socket = new Socket("localhost", 8000);
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
BufferedReader br = getReader(socket);
PrintWriter pw = getWrite(socket);

String msg = null;
while((msg = in.readLine()) != null) {
System.out.println("client:" + msg);
pw.println(msg);
System.out.println(br.readLine());

if("bye".equals(msg)) {
break;
}
}
}catch(Exception e) {
e.printStackTrace();
}finally {
if(socket != null) {
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

private static BufferedReader getReader(Socket socket) throws IOException {
InputStream is = socket.getInputStream();
return new BufferedReader(new InputStreamReader(is));
}

private static PrintWriter getWrite(Socket socket) throws IOException {
OutputStream os = socket.getOutputStream();
return new PrintWriter(os, true); //注意true参数,否则数据不会马上发送,出现收不到的情况
}
}
a>为每一个客户生产一个服务线程:
public class CreateThreadForEveryClient {
private int port = 8000;
private ServerSocket serverSocket = null;

public CreateThreadForEveryClient() throws IOException {
serverSocket = new ServerSocket(port);
System.out.println("服务器启动成功");
}

public void service() {
Socket socket = null;
try {
socket = serverSocket.accept();
Thread t = new Thread(new Handler(socket)); //每收到一个连接就起一个线程。
t.start();
} catch (IOException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws IOException {
new CreateThreadForEveryClient().service();
}
}

class Handler implements Runnable {

public Handler(Socket socket) {
this.socket = socket;
}

private Socket socket = null;

@Override
public void run() {
try {
System.out.println("a client connection:" + socket.getInetAddress() + ":" + socket.getPort());
BufferedReader br = getReader(socket);
PrintWriter pw = getWrite(socket);

String msg = null;
while((msg = br.readLine()) != null) {
System.out.println("server:" + msg);
pw.println(echo(msg));

if("bye".equals(msg)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(socket != null) {
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

private BufferedReader getReader(Socket socket) throws IOException {
InputStream is = socket.getInputStream();
return new BufferedReader(new InputStreamReader(is));
}

private PrintWriter getWrite(Socket socket) throws IOException {
OutputStream os = socket.getOutputStream();
return new PrintWriter(os, true);
}

private String echo(String msg) {
return "echo" + msg;
}
}
b>创建线程池:
上面方法为每个客户端分配一个工作线程,通信结束后线程销毁。以上方法不足之处有:
1>如果许多客户端连接服务器。并且每次通信的时间都很短。那么线程创建和销毁的成本比通信成本还要高。
2>活动的线程会消耗系统资源,如内存(每个线程大概消耗1M内存)。为每个客户端分配线程,如果客户端的数量很多,那么分配的工作线程会很多。销毁大量系统资源。
3>如果线程数目固定,并且执行周期很长。那么线程切换(线程切换指线程之间转让cup的使用权)的频率大概是固定的(一般20毫秒左右),如果线程频繁创建销毁,那么它的切换周期也不在固定。因为销毁的线程必须把cup转让给等待中的线程。线程切换的开销甚至比创建,销毁线程的开销还要大。
线程池解决了线程生命周期开销和系统资源不足的问题。线程池会预先生成几个线程。它们不断的从工作队列中取任务,处理完后在取下一个任务。该方法的好处有:1.减少了创建,销毁线程的次数。因为线程程的线程数是固定的。2.可以手工设定线程池的线程个数,从而根据系统资源的情况灵活调整。
如下例程是线程池的一个解决方案:
ThreadPool.java:
public class ThreadPool extends ThreadGroup {
private static int threadPoolId;
private int threadId;
private LinkedList<Runnable> workQueue;
private boolean isClose = false;

public ThreadPool(int threadNum) {
super("thread pool-" + (threadPoolId++));
setDaemon(true);
workQueue = new LinkedList<Runnable>();
for(int i=0; i<threadNum; i++) {
new WorkThread().start();
}
}

//向工作队列中加入一个任务
public synchronized void execute(Runnable task) {
if(isClose == true) {
throw new IllegalStateException();
}
workQueue.add(task);
notify(); //唤醒工作线程取任务
}

//从工作队列中取任务
public synchronized Runnable getTask() throws InterruptedException {
while(workQueue.size() == 0) {
if(isClose) return null;
wait();
}
return workQueue.removeFirst(); //如果队列不空则返回第一个任务
}

//立即关闭线程池
public synchronized void close() {
if(isClose == false) {
isClose = true;
workQueue.clear();
interrupt();
}
}

//所有线程执行完毕后,关闭线程池
public void join() {
synchronized(this) {
isClose = true;
notifyAll();
}

Thread[] threads = new Thread[activeCount()];
int count = enumerate(threads);
for(int i=0; i<count; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

private class WorkThread extends Thread {

public WorkThread() {
super(ThreadPool.this, "thread:" + threadId++);
}

@Override
public void run() {
Runnable task = null;
while(!isInterrupted()) {
try {
task = getTask();
} catch (InterruptedException e) {
}

if(task == null) return; //如果task==null,或者抛出异常则返回。

task.run(); //取出任务,执行任务。
}
}
}
}
ThreadPoolTest.java:
public class ThreadPoolTest {

public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(3);

for(int i=0; i<5; i++) {
threadPool.execute(createTask(i));
}

threadPool.join();
}

public static Runnable createTask(final int taskId) {
return new Runnable() {

@Override
public void run() {
System.out.println("task"+taskId+":start");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("task"+taskId+":end");
}
};
}
}
c>使用JDK提供的线程池:
java.util.concurrent包提供了现成的线程池,Executor接口表示线程池,它的execute(Runnable task)方法用来执行Runnable类型的任务。 ExecutorService是Executor的子类,它提供了对线程池的一些操作,如shutdown()方法等。Executors提供了一些静态方法,用来生成ExecutorService实例。Executors生成的实例如下:
1>newCachedThreadPool():只有任务时才生成实例,空闲实例存活60S
2>newFixedThreadPool(int threads): 线程池中含有固定数目的线程,线程不会销毁。threads表示线程的个数。
3>newSingleThreadExecutor():只有一个线程,它依次执行每个任务。
4>newScheduleThreadPool(int corePoolSize): 线程会按照时间计划来执行任务。corePoolSize表示最小的线程数,当线程不足时可以创建线程。
5>newSingleThreadScheduleExecutor():线程池中只有一个线程,它会按照时间计划执行任务。
使用JDK的线程池例程如下:
public class JDKThreadPoolServer {
private int port = 8000;
private ServerSocket serverSocket = null;
private ThreadPool threadPool;
private final int POOL_SIZE = 4;
private ExecutorService executorService;

public JDKThreadPoolServer() throws IOException {
serverSocket = new ServerSocket(port);
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * POOL_SIZE);
System.out.println("服务器启动成功");
}

public void service() {
Socket socket = null;
try {
socket = serverSocket.accept();
executorService.execute(new Handler(socket));
} catch (IOException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws IOException {
new JDKThreadPoolServer().service();
}
}

class Handler implements Runnable {

public Handler(Socket socket) {
this.socket = socket;
}

private Socket socket = null;

@Override
public void run() {
try {
System.out.println("a client connection:" + socket.getInetAddress() + ":" + socket.getPort());
BufferedReader br = getReader(socket);
PrintWriter pw = getWrite(socket);

String msg = null;
while((msg = br.readLine()) != null) {
System.out.println("server:" + msg);
pw.println(echo(msg));

if("bye".equals(msg)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(socket != null) {
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

private BufferedReader getReader(Socket socket) throws IOException {
InputStream is = socket.getInputStream();
return new BufferedReader(new InputStreamReader(is));
}

private PrintWriter getWrite(Socket socket) throws IOException {
OutputStream os = socket.getOutputStream();
return new PrintWriter(os, true);
}

private String echo(String msg) {
return "echo" + msg;
}
}
d>使用线程池的注意事项:
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  网络编程