Java网络编程——9.服务器Socket

Java ServerSocket详解
本文详细介绍了Java中的ServerSocket类,包括使用ServerSocket的基本流程、处理客户端连接的不同方式、Socket选项配置、日志记录方法及一个简单的HTTP服务器示例。

前一章从客户端的角度讨论了Socket,客户端就是向监听连接的服务器打开一个Socket的程序,不过如果不能与服务器对话,客户端并没有什么用处。对于接受连接的服务器,Java提供了一个ServerSocket类表示服务器Socket,基本来说服务器Socket的任务就是坐在电话旁等电话。从技术上讲,服务器Socket在服务器上运行,监听入站TCP连接,每个服务器Socket监听服务器机器上的一个特定端口。

1、使用ServerSocket

在Java中,服务器Socket程序的基本生命周期如下:

  1. 使用一个ServerSocket()构造函数在一个特定的端口创建一个新的ServerSocket。
  2. ServerSocket使用其accept()方法监听这个端口的入站连接,accept()会一直阻塞,直到一个客户端尝试建立连接,此时accept()将返回一个连接客户端和服务器的Socket对象。
  3. 根据服务器的类型,会调用Socket的getInputStream()方法或getOutputStream()方法,获得与客户端通信的输入和输出流。
  4. 服务器和客户端根据已协商的协议交互, 直到要关闭连接。
  5. 服务器或客户端关闭连接。
  6. 服务器返回步骤2,等待下一次连接。

下面是一个daytime服务器的例子:

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

public class TestSocket {

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(13)) {
            while (true) {
                try (Socket connection = serverSocket.accept()) {
                    Writer out = new OutputStreamWriter(connection.getOutputStream());
                    Date now = new Date();
                    out.write(now.toString() + "\r\n");
                    out.flush();
                    connection.close();
                } catch (Exception e) {
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

这里的服务器发送最多几十个字节,然后关闭连接,非常快。但可能会有一个很慢或者崩溃的客户端使用服务器挂起几秒的时间,直到它注意到socket已经中断,如果发送数据花费大量时间,你肯定不希望每个连接都必须等待。这里的解决方案是为每个连接提供它自己的一个线程,与接收入站连接放入队列的那个线程分开,防止一个慢客户端阻塞所有其他客户端。这是一种“每个连接对应一个线程”的设计。

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

public class TestSocket {

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(13)) {
            while (true) {
                try {
                    Socket connection = serverSocket.accept();
                    Thread task = new DaytimeThread(connection);
                    task.start();
                } catch (IOException e) {
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static class DaytimeThread extends Thread {
        private Socket connection;

        public DaytimeThread(Socket connection) {
            this.connection = connection;
        }

        @Override
        public void run() {
            try {
                Writer out = new OutputStreamWriter(connection.getOutputStream());
                Date now = new Date();
                out.write(now.toString() + "\r\n");
                out.flush();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    connection.close();
                } catch (IOException e) {
                }
            }
        }

    }

}

不过,这个服务器上有可能发生一种拒绝服务攻击(denial-of-service),由于上述示例为每个连接生成一个新线程,大量几乎同时入站连接可能导致它生成极大数量的线程,最终Java虚拟机会耗尽内存而崩溃。一种更好的办法是使用一个固定的线程池来限制可能的资源使用,假设最多50个线程,它可能会拒绝连接,但起码不会崩溃。

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestSocket {

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(50);

        try (ServerSocket serverSocket = new ServerSocket(13)) {
            while (true) {
                try {
                    Socket connection = serverSocket.accept();
                    Callable<Void> task = new DaytimeTask(connection);
                    pool.submit(task);
                } catch (IOException e) {
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static class DaytimeTask implements Callable<Void> {
        private Socket connection;

        public DaytimeTask(Socket connection) {
            this.connection = connection;
        }

        @Override
        public Void call() throws Exception {
            try {
                Writer out = new OutputStreamWriter(connection.getOutputStream());
                Date now = new Date();
                out.write(now.toString() + "\r\n");
                out.flush();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    connection.close();
                } catch (IOException e) {
                }
            }
            return null;
        }

    }

}

在目前为止的所有例子中,服务器只写入客户端socket,而没有从客户端读取。不过大多数协议要求服务器同时读写客户端,下面将会给出相关例子。如果使用完一个服务器Socket,就应当将它关闭,这会释放端口,使其他希望使用这个端口的程序可以使用。

2、日志

服务器要在无人看管的情况下运行很长时间,通常需要在很久之后对服务器中发生的情况进行调试,这很重要。建议使用Logger logger = LoggerFactory.getLogger(“sampleName”)来打印日志,具体用法不在这里细述。

3、构造服务器Socket

服务器Socket的构造函数可以指定端口、保存入站连接请求所用的队列的长度,以及要绑定的本地网络接口。例如,在端口80创建服务器Socket可以写为ServerSocket server=new ServerSocket (80),队列一次最多可以保存50个入站连接,则写为ServerSocket server=new ServerSocket (80,50),如果试图将队列长度设置为大于操作系统的最大队列长度,则会使用最大队列长度。

如果一个主机有多个网络接口或IP地址,服务器Socket会在所有接口和IP地址的指定端口上监听。不过,还可以增加第3个参数,要求只绑定一个特定的本地IP地址。如:

InetAddress local=InetAddress.getByName("192.168.210.122");
ServerSocket server=new ServerSocket (80,50,local);

所有在这3个构造函数中,可以为端口号传入0,这样系统就会为你选择可用的端口,像这样由系统选择的端口有时称为匿名端口(anonymous port),这对于多socket协议(如FTP)很有用。

无参数构造函数会创建一个ServerSocket对象,但未将它具体绑定到某个端口,所以初始时它不能接受任何连接,之后可以用bind()来进行绑定。这个特性的主要用途是,允许程序在绑定端口之前设置服务器socket选项,有些选项在服务器socket绑定后必须固定。

4、获得服务器socket的有关信息

ServerSocket类提供了 两个获取方法,可以指出这个服务器Socket占用的本地地址和端口,如果你在一个匿名端口或一个未指定的网络接口上打开一个服务器socket,这会很有用。

5、Socket选项

Socket选项指定了ServerSocket类所依赖的原生Socket如何发送和接收数据,对于服务器Socket,Java支持一线3个选项:

  • SO_TIMEOUT:accept()在抛出java.io.InterruptedIOException异常前等待入站连接的时间,一般使用它的默认值0,表示永远不超时。
  • SO_REUSEADDR:跟客户端Socket的选项类似,表示是否允许一个新的Socket绑定到之前使用过的一个端口,而此时可能还有一些发送到原Socket的数据正在网络上传输。
  • SO_RCVBUF:设置服务器Socket接受的客户端Socket默认接收缓冲区大小。

不同的Internet服务类型有不同的性能需求,例如,体育运动的直播视频需要相对较高的带宽,另一方面,电影可能仍需要高带宽,但是可以接受较大的延迟,电子邮件可以通过低带宽的连接传递,甚至延迟几个小时也不会造成大的危害。为TCP定义了4个通用业务流类型:

  • 低成本
  • 高可靠性
  • 最大吞吐量
  • 最小延迟

对于服务器所接受的Socket,setPerformancePreferences()方法描述了为其连接时间、延迟和带宽给定的相对优先级。

public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)

例如通过将connectionTime设置为2,latency设置为1,bandwidth设置为3,表示最大带宽是最重要的特性,最小延迟最不重要,连接时间居中。ss.setPerformancePreferences(2,1,3)。至于给定的VM究竟如何实现,则取决于具体的Socket实现,底层Socket实现不要求考虑这些需求。它们只是为TCP栈提供了所需策略的一个提示。

6、HTTP服务器

HTTP是个大协议,提供完备功能的HTTP服务器必须响应文件请求,将URL转换为本地系统的文件名,响应POST和GET请求,处理不存在的文件的请求,解释MIME类型等。大多数HTTP服务器都是受带宽和延迟限制,而不是CPU速度的限制,因此Java的其他优点,如半编译/半解释特性、动态类加载、垃圾回收和内存保护得到了发挥的机会,特别是有些网站通过servlet或其他机制大量使用了动态内容,会在Java的Web服务器上运行的更快,这是Java可与C竞争的一个领域。利用Java的网络类库,我们可以编写简单的定制服务器。下面给出一个例子:

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JHTTP {

    private static final Logger logger = LoggerFactory.getLogger(JHTTP.class.getCanonicalName());

    private static final int NUM_THREADS=50;
    private static final String INDEX_FILE="index.html";
    
    private final File rootDirectory;
    private final int port;
    
    /**
     * @param rootDirectory
     * @param port
     * @throws IOException 
     */
    public JHTTP(File rootDirectory, int port) throws IOException {
        if(!rootDirectory.isDirectory()){
            throw new IOException("根目录不是以一个目录的形式存在");
        }
        this.rootDirectory = rootDirectory;
        this.port = port;
    }
    
    public void start() throws IOException {
        ExecutorService pool=Executors.newFixedThreadPool(NUM_THREADS);
        try(ServerSocket server=new ServerSocket(port)){
            logger.info("端口:"+server.getLocalPort());
            logger.info("根目录:"+rootDirectory);
            
            while(true){
                try {
                    Socket request=server.accept();
                    Runnable runnable=new RequestProcessor(rootDirectory, INDEX_FILE, request);
                    pool.submit(runnable);
                } catch (Exception e) {
                    logger.error("接收连接异常",e);
                }
            }
            
        }
    }
    
    public static void main(String[] args) {
        File docroot;//得到文档根
        try {
            docroot=new File(args[0]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("使用java JHTTP docroot port");
            return;
        }
        
        int port;//设置要监听的端口
        try {
            port=Integer.parseInt(args[1]);
            if(port<0||port>65535){
                port=8080;
            }
        } catch (RuntimeException e) {
            port=8080;
        }
        
        try {
            JHTTP webserver=new JHTTP(docroot,port);
            webserver.start();
        } catch (Exception e) {
            logger.error("服务器启动异常",e);
        }
        
    }
    
}

处理HTTP请求的runnable类:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;
import java.net.URLConnection;
import java.nio.file.Files;
import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RequestProcessor implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(RequestProcessor.class.getCanonicalName());

    private File rootDirectory;
    private String indexFileName = "index.html";
    private Socket connection;

    public RequestProcessor(File rootDirectory, String indexFileName, Socket connection) {
        if (rootDirectory.isFile()) {
            throw new IllegalArgumentException("根目录必须是目录,不是文件");
        }

        try {
            rootDirectory = rootDirectory.getCanonicalFile();
        } catch (Exception e) {
        }

        this.rootDirectory = rootDirectory;

        if (indexFileName != null) {
            this.indexFileName = indexFileName;
        }
        this.connection = connection;
    }

    @Override
    public void run() {
        // 安全检查
        String root = rootDirectory.getPath();
        try {
            OutputStream raw = new BufferedOutputStream(connection.getOutputStream());
            Writer out = new OutputStreamWriter(raw);
            Reader in = new InputStreamReader(new BufferedInputStream(connection.getInputStream()));
            StringBuilder requestLine = new StringBuilder();
            while (true) {
                int c = in.read();
                if (c == '\r' || c == '\n') {
                    break;
                }
                requestLine.append((char) c);
            }

            String get = requestLine.toString();

            logger.info(connection.getRemoteSocketAddress() + " " + get);
            String[] tokens = get.split("\\s+");
            String method = tokens[0];
            String version = "";
            if (method.equals("GET")) {
                String fileName = tokens[1];
                if (fileName.endsWith("/")) {
                    fileName += indexFileName;
                }
                String contentType = URLConnection.getFileNameMap().getContentTypeFor(fileName);
                if (tokens.length > 2) {
                    version = tokens[2];
                }

                File theFile = new File(rootDirectory, fileName.substring(1, fileName.length()));
                // 不要让客户端超出文档根之外
                if (theFile.canRead() && theFile.getCanonicalPath().startsWith(root)) {
                    byte[] theData = Files.readAllBytes(theFile.toPath());
                    if (version.startsWith("HTTP/")) {
                        // 发送一个MIME首部
                        sendHeader(out, "HTTP/1.0 200 OK", contentType, theData.length);
                    }
                    raw.write(theData);
                    raw.flush();
                } else {
                    // 无法找到文件
                    String body = new StringBuilder("<HTML>\r\n").append("<HEAD><TITLE>File Not Found</TITLE>\r\n")
                            .append("</HEAD>\r\n").append("<BODY>")
                            .append("<H1>HTTP Error 404:File Not Found</H1>\r\n").append("</BODY></HTML>\r\n")
                            .toString();
                    if (version.startsWith("HTTP/")) {
                        // 发送一个MIME首部
                        sendHeader(out, "HTTP/1.0 404 File Not Found", "text/html;charset=utf-8", body.length());
                    }
                    out.write(body);
                    out.flush();
                }
            } else {
                // 不是get方法
                String body = new StringBuilder("<HTML>\r\n").append("<HEAD><TITLE>Not Implemented</TITLE>\r\n")
                        .append("</HEAD>\r\n").append("<BODY>").append("<H1>HTTP Error 501:Not Implemented</H1>\r\n")
                        .append("</BODY></HTML>\r\n").toString();
                if (version.startsWith("HTTP/")) {
                    // 发送一个MIME首部
                    sendHeader(out, "HTTP/1.0 501 Not Implemented", "text/html;charset=utf-8", body.length());
                }

                out.write(body);
                out.flush();
            }

        } catch (Exception e) {
            logger.error("Error talking to " + connection.getRemoteSocketAddress(), e);
        } finally {
            try {
                connection.close();
            } catch (IOException ex) {
            }
        }
    }

    private void sendHeader(Writer out, String responseCode, String contentType, int length) throws IOException {
        out.write(responseCode + "\r\n");
        Date now = new Date();
        out.write("Date:" + now + "\r\n");
        out.write("Server:JHTTP 2.0\r\n");
        out.write("Content-length:" + length + "\r\n");
        out.write("Content-type:" + contentType + "\r\n\r\n");
        out.flush();
    }

}

这个服务器还可以添加以下的一些特性:

  • 服务器管理界面
  • 支持Java Servlet API
  • 支持其他请求方法,如POST、PUT等
  • 支持多个文档根目录

如果真的希望使用JHTTP来运行一个高流量的网站,还要做很多事情来优化,比如实现智能缓存,使用非阻塞I/O和通道来代替线程和流。

转载于:https://my.oschina.net/zhaoyi1/blog/888213

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值