Java中的网络编程

连接到服务器

用Java连接到服务器

下面程序与使用telnet工具是相同的,即连接到某个端口并打印出它所找到的信息

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class SocketTest {

    public static void main(String[] args) throws IOException {
        Socket s = new Socket("time-a.nist.gov",13);
        Scanner scanner = new Scanner(s.getInputStream(), String.valueOf(StandardCharsets.UTF_8));
        while (scanner.hasNextLine()) {
            String line = scanner.nextLine();
            System.out.println(line);
        }
    }

}

其中,

Socket s = new Socket("time-a.nist.gov",13);
InputStream inStream = s.getInputStream();

第一行代码用于打开一个套接字,它也是网络软件中的一个抽象概念,负责启动该程序内部和外部之间的通信。我们将远程地址和端口号传递给套接字的构造器,如果连接失败,它将抛出一个UnknownHostException异常;如果存在其他问题,它将抛出一个IOException异常。

一旦套接字被打开,java.net.Socket类中的getInputStream方法就会返回一个InputStream对象,该对象可以像其他任何流对象一样使用。而一旦获取了这个流,该程序将直接把每一行打印到标准输出。这个过程将一直持续到流发送完毕且服务器断开连接为止。

Socket类非常简单易用,因为Java库隐藏了建立网络连接和通过连接发送数据的复杂过程。实际上,java.net包提供的编程接口与操作文件时所使用的接口基本相同。

套接字超时

从套接字读取信息时,在有数据可供访问之前,读操作将会被阻塞。如果此时主机不可达,那么应用将要等待很长时间,并且因为受底层操作系统的限制而最终会导致超时。

对于不同的应用,应该确定合理的超时值。然后调用setSoTimeout方法设置这个超时值(单位:毫秒)。

var s = new Socket(...);
s.setSoTimeout(10000); //time out after 10 seconds

如果已经为套接字设置了超时值,并且之后的读操作和写操作在没有完成之前就超过了时间限制,那么这些操作就会抛出SocketTimeoutException异常。可以捕获这个异常,并对超时做出反应:

try {
	InputStream in = s.getInputStream();
	...
}
catch(SocketTimeoutException e) {
	react to timeout
}

另外还有一个超时问题是必须解决的。下面这个构造器:

Socket(String host, int port)

会一直无限期地阻塞下去,直到建立了到达主机的初始连接为止。

可以通过先构建一个无连接的套接字,然后再使用一个超时来进行连接的方式解决这个问题。

var s = new Socket();
s.connect(new InetSocketAddress(host,port),timeout);

因特网地址

如果需要在主机名和因特网地址之间进行转换,那么就可以使用InetAddress类。

只要主机操作系统支持IPv6格式的因特网地址,java.net包也将支持它。

静态的getByName方法可以返回代表某个主机的InetAddress对象。例如:

InetAddress address = InetAddress.getByName("time-a.nist.gov");

将返回一个InetAddress对象,该对象封装了一个4字节的序列:129.6.15.28。然后,可以使用getAddress方法来访问这些字节:

byte[] addressBytes = address.getAddress();

一些访问量较大的主机名通常会对应于多个因特网地址,以实现负载均衡。当访问主机时,会随机选取其中一个。可以通过调用getAllByName方法来获得所有主机:

InetAddress[] address = InetAddress.getAllByName(host);

最后需要说明的是,有时我们可能需要本地主机的地址。如果只是要求得到localhost的地址,那总会得到本地回环地址127.0.0.1,但是其他程序无法用这个地址来连接到这台机器上。此时,可以使用静态的getLocalHost方法来得到本地主机的地址:

InetAddress address = InetAddress.getLocalHost();

实现服务器

服务器套接字

一旦启动了服务器程序,它便会等待某个客户端连接到它的端口:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class EchoServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8189);
        Socket accept = serverSocket.accept();
        InputStream inputStream = accept.getInputStream();
        OutputStream outputStream = accept.getOutputStream();

        Scanner in = new Scanner(inputStream, String.valueOf(StandardCharsets.UTF_8));
        PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8),true);

        out.println("hello enter bye to exit.");

        boolean done = false;
        while (!done && in.hasNextLine()) {
            String line = in.nextLine();
            out.println("Echo: "+line);
            if (line.trim().equals("BYE")) done = true;
        }
    }

}

其中,ServerSocket类用于建立套接字:

var s = new ServerSocket(8189);

用于建立一个负责监控接口8189的服务器。

Socket accept = s.accept();

用于告诉程序不停地等待,直到有客户端连接到这个端口。一旦有人通过网络发送了正确的连接请求,并以此连接到了端口上,该方法就会返回一个表示连接已经建立的Socket对象。你可以使用这个对象来得到输入流和输出流:

InputStream inStream = accept.getInputStream();
OutputStream outStream = accept.getOutputStream();

服务器发送给服务器输出流的所有信息都会成为客户端程序的输入,同时来自客户端程序的所有输出都会被包含在服务器输入流中。

程序中,因为要用套接字来发送文本,所以我们将流转换成扫描器和写入器。

var in = new Scanner(inStream,StandardCharset.UTF_8);
var out = new PrintWriter(new OutputStreamWriter(outStream,StandardCharset.UTF_8),true);

在代码最后,我们关闭了连接进来的套接字。

accept.close();

每一个服务器程序,比如一个HTTP web服务器,都会不间断地执行下面这个循环:

  1. 通过输入数据流从客户端接收一个命令。
  2. 解码这个客户端命令。
  3. 收集客户端所请求的信息。
  4. 通过输出数据流发送信息给客户端。

为多个客户端服务

每当程序建立一个新的套接字连接,也就是说当调用accept方法时,将会启动一个新的线程来处理服务器和该客户端之间的连接,而主程序将立即返回并等待下一个连接。为了实现这种机制,服务器应该具有类似下面代码的循环操作:

while(true) {
	Socket incoming = s.accept();
	var r = new ThreadedEchoHandler(incoming);
	var t = new Thread(r);
	t.start();
}

ThreadedEchoHandler类实现了Runnable接口,而且在它的run方法中包含了与客户端循环通信的代码。

由于每个连接都会启动一个新的线程,因而多个客户端就可以同时连接到服务器了。

package internet;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class ThreadedEchoServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8189);

        int i = 1;
        while (true) {
            Socket accept = serverSocket.accept();
            System.out.println("spawning "+i);
            Runnable r = new ThreadedEchoHandler(accept);
            Thread thread = new Thread(r);
            thread.start();
            i++;
        }
    }

}

class ThreadedEchoHandler implements Runnable {
    private Socket incoming;

    public ThreadedEchoHandler(Socket incoming) {
        this.incoming = incoming;
    }

    @Override
    public void run() {
        try {
            InputStream inputStream = incoming.getInputStream();
            OutputStream outputStream = incoming.getOutputStream();
            Scanner in = new Scanner(inputStream, String.valueOf(StandardCharsets.UTF_8));
            PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), true);

            out.println("hello. enter BYE to exit.");
            boolean done = false;
            while (!done && in.hasNextLine()) {
                String line = in.nextLine();
                out.println("echo: "+line);
                if (line.trim().equals("BYE")) {
                    done = true;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

半关闭

半关闭(half-close)提供了这样一种能力:套接字连接的一端可以终止其输出,同时仍旧可以接收来自另一端的数据。

这是一种很典型的情况,例如我们向服务器传输数据,但是一开始并不知道要传输多少数据。在向文件写数据时,我们只需在数据写入后关闭文件即可。但是,如果关闭一个套接字,那么与服务器的连接将立即断开,因而也就无法读取服务器的响应了。

使用半关闭的方法就可以解决上述问题。可以通过关闭一个套接字的输出流来表示发送给服务器的请求数据已经结束,但是必须保持输入流处于打开状态。

如下代码演示了如何在客户端使用半关闭方法:

try (var socket = new Socket(host,port)) {
	var in = new Scanner(socket.getInputStream(),StandardCharset.UTF_8);
	var writer = new PrintWriter(socket.getOutputStream());
	//send request data
	writer.print(...);
	writer.flush();
	socket.shutdownOutput();
	// now socket is half-closed
	while(in.hasNextLine() != null) {
		String line = in.nextLine();
		...
	}
}

服务器端将读取输入信息,直至到达输入流的结尾,然后它再发送响应。

当然,该协议只适用于一站式的服务,例如HTTP服务,在这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接。

获取Web数据

URL和URI

URL和URLConnection类封装了大量复杂的实现细节,这些细节涉及如何从远程站点获取信息。例如,可以自一个字符串构建一个URL对象:

var url = new URL(urlString);

如果只是想获得资源的内容,可以使用URL类中的openStream方法。该方法将产生一个InputStream对象,然后就可以按照一般的用法来使用这个对象了,比如构建一个Scanner对象:

InputStream inStream = url.openStream();
var in = new Scanner(inStream,StandardCharset.UTF_8);

java.net包对统一资源定位符(Uniform Resource Locator,URL)统一资源标识符(Uniform Resource Identifier,URI) 进行非常有用的区分。

URI是个纯粹的语法结构,包含用来指定web资源的字符串的各种组成部分。URL是URI的一个特例,它包含了用于定位web资源的足够信息。其他URI,比如

mailto:cay@horstmann.com

则不属于定位符,因为根据该标识符我们无法定位任何数据。像这样的URI我们称之为URN(uniform resource name,统一资源名称)

在Java类库中,URI类并不包含任何用于访问资源的方法,它的唯一作用就是解析
但是,URL类可以打开一个连接到资源的流 。因此,URL类只能作用于那些Java类库知道该如何处理的模式,例如http:,https:,ftp:,本地文件系统(file:)和JAR文件(jar:)。

一个URI具有以下句法:

[scheme:]schemeSpecificPart[#fragment]

上式中,[...]表示可选部分,并且:#可以被包含在标识符内。
包含scheme:部分的URI称为绝对URI 。否则,称为相对URI

如果绝对URI的schemeSpecificPart不是以/开头的,我们就称它是不透明 的。例如:

mailto:cay@horstmann.com

所有绝对的透明URI和所有相对URI都是分层的。例如:

http://horstmann.com/index.html
../../java/net/Socket.html#Socket()

一个分层URI的schemeSpecificPart具有以下结构:

[//authority][path][?query]

在这里,[...]同样表示可选的部分。

对于那些基于服务器的URI,authority部分具有以下形式:

[user-info@]host[:port]

port必须是一个整数。

URI类的作用之一是解析标识符并将它分解成各种不同的组成部分。你可以用以下方法读取它们:

getScheme
getSchemeSpecificPart
getAuthority
getUserInfo
getHost
getPort
getPath
getQuery
getFragment

URI类的另一个作用是处理绝对标识符相对标识符 。如果存在一个如下的绝对URI:

http://docs.mycompany.com/api/java/net/ServerSocket.html

和一个如下的相对URI:

../../java/net/Socket.html#Socket()

那么可以用它们组合出一个绝对URI:

http://docs.mycompany.com/api/java/net/Socket.html#Socket()

这个过程称为解析相对URL

与此相反的过程称为相对化(relativization) 。例如,有一个基本的URI:

http://docs.mycompany.com/api

和另一个URI:

http://docs.mycompany.com/api/java/lang/String.html

那么相对化之后的URI就是:

java/lang/String.html

URI类同时支持以下两个操作:

relative = base.relativize(combined);
combined = base.resolve(relative);

使用URLConnection获取信息

如果想从某个web资源获取更多信息,那么应该使用URLConnection类,通过它能够得到比基本的URL类更多的控制功能。

当操作一个URLConnection对象时,必须像下面这样非常小心地安排操作步骤:

  1. 当调用URLConnection对象时,必须像下面这样非常小心地安排操作步骤:
URLConnection connection = url.openConnection();
  1. 使用以下方法来设置任意的请求属性:
setDoInput
setDoOutput
setIfModifiedSince
setUseCaches
setAllowUserInteraction
setRequestProperty
setConnectTimeout
setReadTimeout
  1. 调用connect方法连接远程资源:
connection.connect()

除了与服务器建立套接字连接外,该方法还可用于向服务器查询头信息。

  1. 与服务器建立连接后,你可以查询头信息。getHeaderFieldKey和getHeaderField这两个方法枚举了消息头的所有字段。getHeaderFields方法返回一个包含了消息头中所有字段的标准Map对象。为了方便使用,以下方法可以查询各标准字段:
getContentType
getContentLength
getContentEncoding
getDate
getExpiration
getLastModified
  1. 最后,访问资源数据。使用getInputStream方法获取一个输入流用以读取信息(这个输入流与URL类中的openStream方法所返回的流相同)。另一个方法getContent在实际操作中并不是很有用。由标准内容类型所返回的对象需要使用com.sun层次结构中的类来进行处理。

示例:

package internet;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

public class URLConnectionTest {

    public static void main(String[] args) throws IOException {
        String urlName = "https://www.baidu.com/";

        URL url = new URL(urlName);
        URLConnection connection = url.openConnection();

        connection.connect();

        Map<String, List<String>> headers = connection.getHeaderFields();
        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
            String key = entry.getKey();
            for (String value : entry.getValue()) {
                System.out.println(key + ":" + value);
            }
        }

        System.out.println("-----------------");
        System.out.println("getContentType: " + connection.getContentType());
        System.out.println("getContentLength: "+connection.getContentLength());
        System.out.println("getContentEncoding: "+connection.getContentEncoding());
        System.out.println("getDate: "+connection.getDate());
        System.out.println("getExpiration: "+connection.getExpiration());
        System.out.println("getLastModified"+connection.getLastModified());
        System.out.println("-----------------");

        String encoding = connection.getContentEncoding();
        if (encoding == null) encoding = "UTF-8";
        Scanner in = new Scanner(connection.getInputStream(), encoding);
        for (int n = 1;in.hasNextLine() && n <= 10;n++) {
            System.out.println(in.nextLine());
        }
        if (in.hasNextLine()) System.out.println("...");
    }

}

输出:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值