文章目录
相关文章:
【网络编程socket】图解 Java NIO BIO MIO AIO 四大IO模型与原理
【网络编程socket】BIO & Socket和ServerSocket API & 入门例子
【网络编程socket】java NIO编程示例以及流程详解
概述
ServerSocket是基于BIO的
1、构造ServerSocket
ServerSocket的构造方法有以下几种重载形式:
ServerSocket()throws IOException
ServerSocket(int port) throws IOException
ServerSocket(int port, int backlog) throws IOException
ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
在以上构造方法中,参数port指定服务器要绑定的端口(服务器要监听的端口),参数backlog指定客户连接请求队列的长度,参数bindAddr指定服务器要绑定的IP地址。
1.1 、绑定端口
除不带参数的构造方法以外,其他构造方法都会使服务器与特定端口绑定,该端口由参数port指定。
如果端口被其他服务进程占用,或是,在某些系统中,若没有以超级用户身份运行服务器程序,操作系统不允许服务器绑定到1-1023的端口时,会抛出BindException。
1.2、设定客户连接请求队列的长度
当服务器进程运行时,可能会同时监听到多个客户的连接请求。管理客户端连接请求的任务是由操作系统来完成的。操作系统将连接请求存储在一个先进先出队列中。许多操作系统限定了队列的最大长度,一般为50
。当队列中的连接请求达到了队列的最大容量时,服务器进程所在的主机会拒绝新的连接请求。只有当服务器进程通过ServerSocket的accept()方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入新的连接请求。
对于客户进程,如果它发出的连接请求被加入到服务器的队列中,就意味着客户与服务器的连接建立成功,客户进程从Socket构造方法中正常返回。如果客户进程发出的连接请求被服务器拒绝,Socket构造方法就会抛出ConnectionException。
ServerSocket构造方法的backlog参数用来显式设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。
在以下几种情况,仍然采用操作系统限定的队列最大长度:
-
backlog参数的值大于操作系统限定的队列的最大长度;
-
backlog参数的值小于或等于0;
-
在ServerSocket构造方法中没有设置backlog参数。
1.3、设定绑定的IP地址
若主机只有一个地址,则服务器默认绑定该地址;若主机有多个地址,则可以调用ServerSocket(int port, int backlog, InetAddress bindAddr)构造方法设置主机ip地址。
1.4、默认构造方法的作用
ServerSocket有一个不带参数的默认构造方法。通过该方法创建的ServerSocket不与任何端口绑定,接下来还需要通过bind()方法与特定端口绑定。
这个默认构造方法的用途是,允许服务器在绑定到特定端口之前,先设置ServerSocket的一些选项。因为一旦服务器与特定端口绑定,有些选项就不能再改变了。
2、接收和关闭与客户的连接
ServerSocket的accept()方法从连接请求队列中取出一个客户的连接请求,然后创建与客户连接的Socket对象,并将它返回。如果队列中没有连接请求,accept()方法就会一直等待,直到接收到了连接请求才返回。
服务器从Socket对象中获得输入流和输出流
,就能与客户交换数据。当服务器正在进行发送数据的操作时,如果客户端断开了连接,那么服务器端会抛出一个IOException的子类SocketException异常:java.net.SocketException: Connection reset by peer。
3、关闭ServerSocket
ServerSocket的close()方法使服务器释放占用的端口,并且断开与所有客户的连接。当一个服务器程序运行结束时,即使没有执行ServerSocket的close()方法,操作系统也会释放这个服务器占用的端口。因此,服务器程序并不一定要在结束之前执行ServerSocket的close()方法。
在某些情况下,如果希望及时释放服务器的端口,以便让其他程序能占用该端口,则可以显式调用ServerSocket的close()方法。
ServerSocket的isClosed()方法判断ServerSocket是否关闭,只有执行了ServerSocket的close()方法,isClosed()方法才返回true;否则,即使ServerSocket还没有和特定端口绑定,isClosed()方法也会返回false。
ServerSocket的isBound()方法判断ServerSocket是否已经与一个端口绑定,只要ServerSocket已经与一个端口绑定,即使它已经被关闭,isBound()方法也会返回true。
4、获取ServerSocket的信息
- public InetAddress getInetAddress():获取服务器绑定的ip地址;
- public int getLocalPort():获取服务器绑定的端口;
在构造ServerSocket时,如果把端口设为0,那么将由操作系统为服务器分配一个端口(称为匿名端口),程序只要调用getLocalPort()方法就能获知这个端口号。多数服务器会监听固定的端口,这样才便于客户程序访问服务器。匿名端口一般适用于服务器与客户之间的临时通信,通信结束,就断开连接,并且ServerSocket占用的临时端口也被释放。
5、ServerSocket选项
ServerSocket有以下3个选项。
- SO_TIMEOUT:表示等待客户连接的超时时间。
- SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。
- SO_RCVBUF:表示接收数据的缓冲区的大小。
5.1、SO_TIMEOUT选项
- 设置该选项: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的子类。
5.2、SO_REUSEADDR选项
- 设置该选项:public void setResuseAddress(boolean on) throws SocketException
读取该选项:public boolean getResuseAddress() throws SocketException
这个选项与Socket的SO_REUSEADDR选项相同,用于决定如果网络上仍然有数据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些操作系统中不允许重用端口。
当ServerSocket关闭时,如果网络上还有发送到这个ServerSocket的数据,这个ServerSocket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口
许多服务器程序都使用固定的端口。当服务器程序关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占用,使得服务器程序无法绑定到该端口,服务器启动失败,并抛出BindException。
为了确保一个进程关闭了ServerSocket后,即使操作系统还没释放端口,同一个主机上的其他进程还可以立刻重用该端口,可以调用ServerSocket.setResuseAddress(true)方法
5.3、SO_RCVBUF选项
- 设置该选项:public void setReceiveBufferSize(int size) throws SocketException
- 读取该选项:public int getReceiveBufferSize() throws SocketException
SO_RCVBUF表示服务器端的用于接收数据的缓冲区的大小,以字节为单位。一般说来,传输大的连续的数据块(基于HTTP或FTP协议的数据传输)可以使用较大的缓冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信(Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。
5.4、设定连接时间、延迟和带宽的相对重要性
public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)
该方法的作用与Socket的setPerformancePreferences()方法的作用相同,用于设定连接时间、延迟和带宽的相对重要性。
6、创建多线程服务器
许多实际应用要求服务器具有同时为多个客户提供服务的能力。HTTP服务器就是最明显的例子。任何时刻,HTTP服务器都可能接收到大量的客户请求,每个客户都希望能快速得到HTTP服务器的响应。如果长时间让客户等待,会使网站失去信誉,从而降低访问量。
可以用并发性能来衡量一个服务器同时响应多个客户的能力。一个具有好的并发性能的服务器,必须符合两个条件:
- 能同时接收并处理多个客户连接;
- 对于每个客户,都会迅速给予响应。
用多个线程来同时为多个客户提供服务,这是提高服务器的并发性能的最常用的手段。
以下将按照3中方式来实现EchoServer,它们都使用多线程。
- 为每个客户分配一个工作线程。
- 使用线程池,由其中的工作线程来为客户服务。
可以利用JDK的Java类库中现成的线程池,由它的工作线程来为客户服务。
6.1、 为每个客户分配一个线程
服务器的主线程负责接收客户的连接,每次接收到一个客户连接,就会创建一个工作线程,由它负责与客户的通信。
public static void start(){
try{
ServerSocket serverSocket = new ServerSocket(PORT);
System.out.println("server listen on port:" + PORT);
while (true){
try {
Socket client = serverSocket.accept();
System.out.println("receive client connect, localPort=" + client.getPort());
//每次都new一个新的线程
new Thread(new EchoServer.HandlerServer(client)).start();
}catch (Exception e){
System.out.println("client exception,e=" + e.getMessage());
}
}
}catch(Exception e){
System.out.println("server exception,e=" + e.getMessage());
}
}
6.2、使用JDK类库提供的线程池
static ExecutorService executorServices = Executors.newCachedThreadPool();
7. 完整的入门例子
服务端:
package com.io.bio;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BioServer {
private static int PORT = 8080;
private static ServerSocket server;
static ExecutorService executorServices = Executors.newCachedThreadPool();
public static void main(String[] args) throws IOException{
if(server != null) return;
try{
//创建一个服务端,构造方法内自动完成bind,listen操作
server = new ServerSocket(PORT);
System.out.println("服务器启动成功,准备接受请求");
while(true){
//等待客户端的connector,如果没有新的连接,就一直阻塞
Socket socket = server.accept();
//加入线程池,负责处理消息
executorServices.submit(new ServerHandler(socket));
}
}finally{
if(server != null){
System.out.println("服务器已关闭。");
server.close();
server = null;
}
}
}
}
/**
* 负责处理消息
*/
class ServerHandler implements Runnable{
private Socket socket;
public ServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream in = null;
OutputStream out = null;
try{
byte[] recv = new byte[1024];
in = socket.getInputStream();
//注意:这个read方法会阻塞,如果客户端连接过来后,迟迟不发消息过来,那么服务端此次就一直阻塞下去
in.read(recv);
System.out.println("服务器收到消息:" + new String(recv));
out = socket.getOutputStream();
out.write("hello client ".getBytes());
}catch(Exception e){
e.printStackTrace();
}finally{
if(in != null){
try {
in.close();
} catch (Exception e) {
e.printStackTrace();
}
in = null;
}
if(in != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
out = null;
}
if(socket != null){
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
客户端:
package com.io.bio;
import java.io.*;
import java.net.Socket;
public class BioClient {
private static int SERVER_PORT = 8080;
private static String SERVER_IP = "127.0.0.1";
public static void main(String[] args){
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try{
socket = new Socket(SERVER_IP,SERVER_PORT);
String hello = "hello server";
OutputStream outputStream = socket.getOutputStream();
outputStream.write(hello.getBytes());
outputStream.flush();
InputStream inputStream = socket.getInputStream();
byte[] rev = new byte[1024];
inputStream.read(rev);
System.out.println("client receive: "+new String(rev));
}catch(Exception e){
e.printStackTrace();
}finally{
if(in != null){
try {
in.close();
} catch (Exception e) {
e.printStackTrace();
}
in = null;
}
if(in != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
out = null;
}
if(socket != null){
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
分别启动服务端和客户端代码:
服务端:
客户端,收到服务端的响应就关闭了: