服务器Socket概述与实例

绝大部分知识与实例来自O’REILLY的《Java网络编程》(Java Network Programming,Fourth Edition,by Elliotte Rusty Harold(O’REILLY))。

ServerSocket简介

ServerSocket类包含了使用Java编写服务器所需的全部内容,其中包括创建新ServerSocket对象的构造函数、在指定端口监听连接的方法、配置各个服务器Socket选项的方法,以及一些常见的方法(如toString)。
在Java中,一个服务器的生命周期如下:

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

下面看一个简单的服务器,用于获取时间:

实例1:Daytime服务器

public static void createDaytimeServer(){
    try(ServerSocket server = new ServerSocket(13)){
        while(true){
            try(Socket connection = server.accept()){
                Writer out = new OutputStreamWriter(
                                connection.getOutputStream(),
                                "UTF-8");
                Date now = new Date();
                out.write(now.toString() + "\r\n");
                out.flush();
            }catch (IOException e) {

            }
        }
    } catch (IOException e) {
        System.err.println("服务器意外退出");
    }
}

启动服务器:

createDaytimeServer();

请求服务器提供服务:

try(Socket socket = new Socket("127.0.0.1", 13)){
    BufferedReader in = new BufferedReader(
            new InputStreamReader(socket.getInputStream(), "UTF-8"));
    System.out.println(in.readLine());
} catch (UnknownHostException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

输出:
Wed Sep 13 09:34:09 CST 2017

注意点有以下几个:

  1. 客户端Socket与服务器Socket使用完毕后都需要调用close()关闭连接,Java 7及之后的版本可以利用try-with-resources实现。
  2. 启动服务器的代码和请求服务器提供服务的代码要分开执行,否则可能得不到输出。
  3. 服务器的代码中有两类异常,一类是某个客户端连接出错时抛出的,一类是服务器出错时抛出的,必须分别处理。
  4. 在同一台机器上测试的话,可以为Socket传入本地回送地址127.0.0.1。

多线程服务器

前面的服务器一次只能处理一个请求,如果有一个很慢的客户端先请求到了服务,会使得后面的客户端阻塞很长时间。一个比较好的方法是为每个连接提供一个线程。

实例2:使用线程池的多线程Daytime服务器

public static void createPooledDaytimeServer(){
    ExecutorService pool = Executors.newFixedThreadPool(50);

    try(ServerSocket server = new ServerSocket(13)){
        while(true){
            try {
                Socket connection = server.accept();
                pool.execute(new DaytimeTask(connection));
            } catch (IOException e) {

            }
        }
    } catch (IOException e) {
        System.err.println("服务器意外退出");
    } 
}

private static class DaytimeTask implements Runnable{
    private Socket connection;

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

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

使用方法和输出结果和实例1基本相同。
注意点有以下几个:

  1. 多线程的情况下,客户端Socket的关闭应当由处理这个连接的线程完成,因为服务器不知道应当什么时候关闭。

日志(暂缺)

ServerSocket的其他构造器

public ServerSocket() throws IOException

public ServerSocket(int port) throws IOException

public ServerSocket(int port, int backlog)

public ServerSocket(int port, int backlog, InetAddress bindAddr)

port指要绑定的端口;如果传入0,系统会自动选择一个可用的端口;
backlog指等待连接队列的最大长度;
bindAddr指一个特定的本地IP地址。默认情况下,如果一个主机有多个网络接口或IP地址,ServerSocket会在每个接口和IP地址上监听。如果设置了bindAddr,那么就只会监听这个地址上的入站连接。
无参构造器在创建对象时不会绑定端口,可以在之后使用bind()方法进行绑定。

性能优先级

利用setPerformancePreferences(int connectionTime,int latency,int bandwidth)可以设置服务器各项性能指标的优先级。例如:setPerformancePreferences(2,1,3)表示最大带宽是最重要的性能,最小延迟最不重要,连接时间居中。

HTTP服务器

构建一个HTTP服务器的关键在于实现HTTP协议,即根据HTTP协议读取客户端的请求报文,并构造出对应的响应报文。下面是一个简单的单文件服务器,它无论接收到什么请求都返回200 OK,并传输一个指定文件。

实例3:单文件HTTP服务器

public class SingleFileHTTPServer {
    private final byte[] content;
    private final byte[] header;
    private final int port = 80;
    private final String encoding = "UTF-8";

    public SingleFileHTTPServer(String data,String mimeType) {
        this(data.getBytes(), mimeType);
    }

    public SingleFileHTTPServer(byte[] data, String mimeType){
        this.content = data;
        //响应报文首部
        String header = "HTTP/1.0 200 OK \r\n"
                + "Server: OneFile 2.0\r\n"
                + "Content-length:" + this.content.length + "\r\n"
                + "Content-type:" + mimeType 
                + "; charset=" + encoding + "\r\n\r\n";
        this.header = header.getBytes(Charset.forName("US-ASCII"));
    }

    public void start(){
        ExecutorService pool = Executors.newFixedThreadPool(10);
        try(ServerSocket server = new ServerSocket(port)){
            while(true){
                try{
                    Socket connection = server.accept();
                    pool.execute(new HTTPHandler(connection));
                }catch (IOException e) {
                    e.printStackTrace();
                }catch (RuntimeException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private class HTTPHandler implements Runnable{
        private final Socket connection;

        HTTPHandler(Socket connection) {
            this.connection = connection;
        }

        @Override
        public void run() {
            try{
                OutputStream out = 
                        new BufferedOutputStream(connection.getOutputStream());
                InputStream in =
                        new BufferedInputStream(connection.getInputStream());

                StringBuilder request = new StringBuilder(80);
                while(true){
                    int c = in.read();
                    if(c == '\r' || c == '\n' || c == -1){
                        break;
                    }
                    request.append((char)c);
                }
                System.out.println(request.toString());
                if(request.toString().indexOf("HTTP/") != -1){
                    out.write(header);
                }
                out.write(content);
                out.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }finally{
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    //客户端测试用,会保存服务器发来的文件
    public static void test(){
        try {
            URL url = new URL("http://127.0.0.1");
            URLConnection connection = url.openConnection();
            InputStream in = new BufferedInputStream(connection.getInputStream());
            int total = Integer.parseInt(connection.getHeaderField("Content-length"));      
            File file = new File("C:\\Users\\qwer\\Desktop" 
            + File.separator + "123.pdf");

            try(FileOutputStream out = new FileOutputStream(file)){
                byte[] bytes = new byte[64 * 1024];
                int size;
                int len = 0;
                while(len < total){
                    size = in.read(bytes);
                    out.write(bytes, 0, size);
                    len += size;
                    System.out.println(len);
                }
                out.flush();
                System.out.println("finished");
            }
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        try {
            String filepath = "C:\\Users\\qwer\\Desktop\\osu\\漫画数据库.pdf";
            Path path = Paths.get(filepath);
            byte[] data = Files.readAllBytes(path);

            //用于获取文件资源的MIME类型
            String contentType = URLConnection.getFileNameMap().getContentTypeFor(filepath);
            SingleFileHTTPServer server = new SingleFileHTTPServer(data, contentType);
            server.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
//      test();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值