网络编程2
ServerSocket用法详解:
﹟构造ServerSocket ServerSocket serverSocket = new ServerSocket(80);
如果运行时无法绑定到80端口,以上代码会抛出IOException.更确切的说,是抛出BindException,它是IOException的子类。
BindException一般是由以下原因造成的: 端口已经被其他服务器进程占用;有某些操作系统中,如果没有以超级用户的身份来运行服务程序,那么操作系统不允许服务器绑定到1-1023之间的端口。
﹟设定客户连接请求队列的长度 serverSocket = new ServerSocket(port,3); //连接请求队列的长度为3 ServerSocket构造方法的backlog参数用来显示设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。
值得注意的是:在以下几种情况,仍然会采用操作系统限定的队列的最大长度:
backlog参数的值大于操作系统限定的队列的最大长度。
backlog参数的值小于或等于0
在ServerSocket构造方法中没有设置backlog参数。
﹟设定绑定的IP地址 serverSocket = new ServerSocket(8000, InetAddress.getByName(“192.168.3.4”));
如果主机只有一个IP地址,那么默认情况下,服务器程序就与该IP地址绑定。
ServerSocket的第四个构造方法 ServerSocket(int port, int backlog, InetAddress bindAddr) 有一个bindAddr参数,它显示只等服务器要绑定的IP地址,该方法适用于具有过个IP地址的主机。
﹟默认构造方法的作用 通过该方法创建的ServerSocket不与任何端口绑定,接下来还需要通过bind()方法与特定的端口绑定。
在以下代码中,先把ServerSocket的SO_REUSEADDR选项设为true, 然后再把它与8000端口绑定: ServerSocket serverSocket = new ServerSocket(); serverSocket.setReuseAddress(true); //设置ServerSocket的选项 serverSocket.bind(new InetSocketAddress(8000)); //与8000端口绑定
﹟接收和关闭与客户的连接
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) {
//这只是与单个客户通信时遇到的异常,可能是由于客户端过早断开连接引起的
//这种异常不应该中断整个while循环
e.printStackTrace();
}finally {
try{
if(socket!=null)socket.close(); //与一个客户通信结束后,要关闭Socket
}catch (IOException e) {e.printStackTrace();}
}
}
}
﹟获取ServerSocket的信息: ServerSocket的以下两个get方法分别获得服务器绑定的IP地址,以及绑定的端口:
public InetAddress getInetAddress() public int getLocalPort()
﹟ServerSocket选项
SO_TIMEOUT: 表示等待客户连接的超时时间。
SO_REUSEADDR: 表示是否允许重用服务器所绑定的地址。
SO_RCVBUF: 表示接收数据的缓冲区的大小。
设置该选项: public void setSoTimeout(int timeout) throws SocketException
读取该选项: public int getSoTimeout() throws IOException
SO_TIMEOUT表示ServerSocket的accept()方法等待客户连接超时时间,以毫秒为单位。如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的默认值。
当服务器执行ServerSocket的accept()方法时,如果连接请求队列为空,服务器就会一直等待,直到接收到了客户连接才从accept()方法返回。如果设定了超时时间,那么服务器等待超时的时间超过了超时时间,就会抛出SocketTimeoutException,它是InterruptedException的子类。
SO_REUSEADDR选项
设置该选项: public void setResuseAddress(boolean on ) throws SocketException
读取该选项: public boolean getReuseAddress() throws SocketException
n 这个选项与Socket的SO_REUSEADDR选项相同,用于决定如果网络上仍然有数据向旧的ServerSocket传输,是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口。
n 为了确保一个进程关闭了ServerSocket后,即使操作系统还没释放端口,同一个主机上的其他进程还可以立刻重用该端口,可以调用ServerSocket的setResuseAddress(true)方法。
n 值得注意的是,serverSocket.setResuseAddress(true)方法必须在ServerSocket还没有绑定到一个本地端口之前调用,否则执行serverSocket.setResuseAddress(true)方法无效。
n 设置该选项:public void setReceiveBufferSize(int size) throws SocketException
n 读取该选项:public int getReceiveBufferSize() throws SocketException
n SO_RCVBUF表示服务器端的用于接收数据的缓冲区的大小,以字节为单位。一般说来,传输大的连续的数据块(比如基于HTTP或FTP协议的数据传输)可以使用较大的缓冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信(比如Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。
n 执行serverSocket.setReceiveBufferSize()方法,相当于对所有由serverSocket.accept()方法返回的Socket设置接收数据的缓冲区的大小。
设定连接、延迟和带宽的相对重要性
n public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)
n 该方法的作用与Socket的setPerformancePreferences()方法的作用相同,用于设定连接时间、延迟和带宽的相对重要性。
﹟创建多线程的服务器
可以用并发性能来衡量一个服务器同时相应多个客户的能力。一个具有好的并发性能的服务器,必须符合两个条件:
能同时接收并处理多个客户连接。 对于每个客户,都会迅速给予相应。
服务器同时处理的客户连接数目越多,并且对每个客户做出响应的速度越快,就表明并发性能越高。
用多个线程来同时为多个客户提供服务,这是提高服务器的并发性能的最常用的手段。本节将按照三种方式来重新实现EchoServer,它们都使用了多线程:
方式一: 为每个客户分配一个工作线程。
方式二: 创建一个线程池,由其中的工作线程来为客户服务。
方式三: 利用JDK的Java类库中现成的线程池,由它的工作线程来为客户服务。
为每一个客户分配一个线程
服务器的主线程负责接收客户的连接,每次接收到一个客户连接,就会创建一个工作线程,由它负责与客户的通信。以下是EchoServer的service()方法的代码;
public void service() {
while(true) {
Socket socket = null;
try {
socket = serverSocket.accept(); //接收客户连接
Thread workThread = new Thread(new Hander(socket)); //创建一个工作线程
workThread.start(); //启动工作线程
}
}
}
创建线程池:
线程池为线程生命周期开销问题和系统资源不足问题提供了解决方案。线程池中预先创建了一些工作线程 它们不断从工作对列中取出任务。然后执行该任务。当工作线程执行完一个任务,就会继续执行工作队列中的下一个任务。
线程池具有以下优点:
减少了创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行过个任务。
可以根据系统的承载能力,方便的调整线程池中线程的数目,防止因为消耗过量系统资源而导致系统崩溃。参见ThreadPool类。
使用JDK类库提供的线程池
java.util.concurrent包提供了现成的线程池的实现
Executor接口表示线程池,它的execute(Runnable task)方法用来执行Runnable类型的任务,Executor的子接口ExecutorService中声明了管理线程池的一些方法,比如用户关闭线程池的shutdown()方法等。 Executors类中包含一些静态方法,它们负责生成各种类型的线程池ExecutorService实例。
EchoServer就利用上述线程池来负责与客户通信的任务。
public EchoServer() throws IOException {
serverSocket = new ServerSocket(port);
//创建线程池
//Runtime的availableProcessors()方法返回当前系统的CPU的数目
//系统的CPU越多,线程池中工作的线程数目也越多
executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors().POOL_SIZE);
System.out.println(服务器启动);
}
﹟使用线程池的注意事项:
线程池可能会带来各种风险:
⒈死锁, 2.系统资源不足 3.并发错误 4.线程泄露 5.任务过载。
使用线程池的注意事项:
使用线程池时需要遵循以下原则:
⒈如果任务A在执行过程中需要同步等待任务B的执行结果,那么任务A不适合加入到线程池的工作队列当中。
2.如果执行某个任务时可能会阻塞,并且是长时间的阻塞,则应该设定超时时间,避免工作线程永久的阻塞下去而导致线程泄露。
3.根据任务的特点,对任务进行分类,然后把不同类型的任务分别加入到不同线程池的工作队列中,这样可以根据任务的特点,分别调整每个线程池。
4.调整线程池的大小。线程池的最佳大小主要取决于系统的可用CPU的数目以及工作队列中任务的特点。
5.避免任务过载。
﹟关闭服务器
n 前面介绍的EchoServer服务器都无法关闭自身,只有依靠操作系统来强行终止服务器程序。这种强行终止服务器程序的方式尽管简单方便,但是会导致服务器中正在执行的任务被突然中断。
n 如果服务器处理的任务不是非常重要,允许随时中断,则可以依靠操作系统来强行终止服务器程序
n 如果服务器处理的任务非常重要,不允许被突然中断,则应该由服务器自身在恰当的时刻关闭自己。
线程是一个程序内部的顺序控制流
线程与进程的区别:
每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销
线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
多进程:在操作系统中能同时运行多个任务。
多线程:在同一应用程序中有多个顺序流同时执行。
Java的线程是通过java.lang.Thread类来实现的。
VM启动时会有一个主方法(public static void main() {})所定义的线程。
可以通过创建Thread的实例来创建新的线程。
每个线程都是通过某个特定Thread对象所对应的run()来完成其操作的,方法run()称为线程体。
通过调用Tread类的start()方法来启动一个线程。
Java中线程的创建和启动
可以有两种方式创建新的进程。
第一种:
定义线程类实现Runnable接口
Thread myThread = new Thread(target) //target为Runnable接口类型
Runnable中只有一个方法;
public void run(); //用以定义线程运行体
使用Runnable接口可以为多个线程提供共享的数据。
在实现Runnable接口的类的run方法定义中可以使用Thread的静态方法。
public static Thrread currentThread()获取当前线程的引用。
第二种
可以定义一个Thread的子类并重写run方法如:
lass MyThread extends Thread {
public void run() {}
}
然后生成该类的对象
MyThread myThread = new MyThread();
线程状态转换图:
线程控制的基本方法
| |
方法 | 功能 |
isAlive | 判断线程是否还”活着”,即线程是否还为终止。 |
getPrioprity() | 获得线程的优先级数值 |
setPrioprity() | 设置线程的优先级值 |
Thread.sleep() | 将当前线程睡眠指定的毫秒数 |
Join() | 调用某线程的该方法,将当前线程与该线程“合并”,即等待该线程结束,再恢复当前线程的运行 |
Yield() | 让出CPU,当前线程进入就绪状态等待调度 |
Wait() | 当线程进入对象的wait pool |
Notify()/notifyAll() | 唤醒对性的wait pool中的一个/所有等待线程 |
sleep方法:
﹟可以调用Thread的静态方法:
public static void sleep(long millis) throws InterruptedException
使得当前线程休眠(暂时停止执行millis毫秒)
由于是静态方法, sleep可以由类名直接调用:
Thread.sleep();
join方法 合并某个线程
yield方法 让出CPU,给其他线程执行的机会。
public interface Runnable
The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread. The class must define a method of no arguments called run.
This interface is designed to provide a common protocol for objects that wish to execute code while they are active.For example, Runnable is implemented by class Thread.Being active simply means that a thread has been started and has not yet been stopped.
In addition, Runnable provides the means for a class to be active while not subclassing Thread. A class that implements Runnable can run without subclassing Thread by instantiating a Thread instance and passing itsellf in as the target. In most cases , the Runnable interface should be used if you are only planning to override the run() method and no other Thread methods. This is important because classes should not be subclassed unlsess the programmer intends on modifying or enhancing the fundamental behavior of the class.
void run()
When an object implementing interface Runnable is used to create a thread starting the thread causes the object’s run method to be called in that separately execuring thread.
Thread A thread is a thread of execution in a program. The Java Virtural Machine allows an application to have multiple threads of execution running concurrently.
Every thread has a priority.Threads with higher priority are executed in preference to threads with lower priority. Each thread may or may not also marked as a daemon. When code running in some thread creates a new Thread object, the new thread has its priority initially set equal to the priority of the creating thread. and is a daemon thread if and only if the creating thread is a daemon.
When a Java Virtural Machine starts up, there is usually a single non-daemon thread (which typically calls the method named main of some designated class). The Java Virtual Machine continues to execute threads until either of the following occurs:
The exit method of class Runtime has been called and the security manager has permitted the exit operation to take place.
All threads that are not daemon threads have died, either by returning from the call to the run method or by throwing an exception that propagates beyond the run method.
pubic static void java.lang.System.exit(int status)
Terminates the currently running Java Virtual Machine. The argument servers as a status code; by convention, a nonzero status code indicates abnormal ternination.
This method calls the exit method in class Runtime. This method never returns normally.
The call System.exit(n) is effectively equivalent to the call:
Runtime.getRuntime().exit(n);
public static void java.lang.System.gc()
Run the garbage collector. Calling the gc method suggests that the Java Virtual Machine expend effort toward recycling unused objects in order to make the memory they currently occupy available for quick reuse. When control returns from the method call, the Java Virtual Machine has made a best effort to reclaim space from all discarded objects;
public class Runtime
Every Java application has a single instance of class Runtime that allows the application to interface with the environment in which the application is running. The currrent runtime can be obtained from the getRuntime method.
Note: An application cannot create its own instance of this class.
public final class Class<T> extends Object
Instances of the class Class represent classes and interfaces in a running Java application. An enum is a kind of class and an annotation is a kind of insterface. Every array also belongs