网络编程涉及到了最基础的Socket编程,以及基于次的网络服务。下面将介绍在Java中如何实现Socket以及一些简单的网络客户端和服务端。
Socket
- 连接
Socket
类是Java中实现的基本的网络编程类,可以通过地址和端口来进行初始化,连接到该服务器的端口上,也可以使用无参构造函数,再调用connect()
方法连接上服务器。两者的区别是当直接使用带参构造函数去初始化Socket
时,进程会一直阻塞直到连接上服务器,而如果是在使用无参构造函数创建Socket
后调用connect()
方法,可以设置连接的时间限制,当超过时间后就会抛出IOException
。
Socket s1=new Socket(String host,int port);
Socket s2=new Socket();
s2.connect(host,port,1000);//时限为1秒钟
- 通信
当Socket
对象连接上服务器后,就可以通过socket.getInputStream()
和socket.getOutputStream()
与服务器进行数据传输。这两个方法返回的是(In|Out)putStream
对象,可以使用输入输出流的处理方法来进行读写。最后通过socket.close()
来关闭。 - 其余方法
此外,Socket
类还实现了一些有助于平常使用的方法。
public InetAddress getInetAddress();//获取socket连接的服务器地址
public int getPort();//获取socket连接的服务器端口
public InetAddress getLocalAddress();//获取socket连接的本地的地址
public int getLocalPort();
public boolean isClosed();
...
ServerSocket
- 创建
服务器程序会在某个端口等待用户程序的连接,在创建ServerSocket
的时候,可以进行端口指定。
创建完ServerSocket
对象后,通过调用s.accept()
方法来等待用户程序连接上服务器。该方法阻塞进程直到用户连接,并返回一个Socket
对象。获取到Socket
对象后就可以通过输入输出流向用户程序进行读写。
ServerSocket ss=new ServerSocket(port);
Socket s=ss.accept();
InputStream in=s.getInputStream();
OutputStream out=s.getOutputStream();
- 并发处理
当要同时处理多个用户程序连接服务器的时候,可以在获得Socket
对象后创建线程来处理该Socket
的读写。
while(ture){
Socket s=ss.accept();
Thread t=new Thread(
public void run(){
//input and output
}
);
t.start();
}
- 半关闭
Socket
对象提供了只关闭输入/输出流其中之一而不断开连接的方法,即shutdown(In|Out)put()
,在断开后,任何输入或输出都将会被抛弃。断开后没有办法再重新连接上,因此这种方法只适用于一站式的服务。
因特网地址
Java中提供了InetAddress
和InetSocketAddress
类来表示因特网地址。其中InetSocketAddress
类单纯地用来表示一个地址,即ip:port
。InetAddress
用来表示一个ip
,并提供了用于获得域名对应的ip等功能。
public boolean isReachable(int timeout) throws IOException;
public String getHostName();
public static InetAddress getByName(String host);
//获取一个域名所有的ip地址
public static InetAddress[] getAllByName(String host);
//获取本地的ipv4地址
public static InetAddress getLocalHost() throws UnknownHostException;
...
获取Web服务
使用Socket
只能与指定的ip:port
进行通信,而无法获得服务器特定目录下的资源,即无法通过URL
获得资源(?)。URI
是统一资源标识符,其中能够定位到数据的称为URL
(统一资源定位符),不能定位到数据的称为URN
(统一资源名称)。
Java提供了URL
类对URL
进行访问,提供了URI
类对标识符进行解析。
-
URI
一个URI
具有这样的格式,[scheme:]schemeSpecificPart[#fragment]
,其中[...]
表示可选的部分,并且:
和#
可以被包含在标识符内。包含[scheme:]
部分的为绝对URI
,否则为相对URI
,其中schemeSpecificPart
以/
开头的绝对URI
为不透明的,例如mailto:cay@test.com
。所有透明的绝对URI和所有相对URI都是分层的,如https://test.com/index
、../../java/net/Socket.html#Socket()
。一个分层的URI的schemeSpecificPart
具有这样的格式,[//authority][path][?query]
,基于服务器的URI的authority
部分具有这样的格式[user-info@]host[:port]
。
URI
类提供了解析标识符和处理相对(绝对)标识符的功能。其中resolve()
方法提供了解析相对URI,将其生成绝对URI的方法,relativize()
方法提供了相对化方法,将绝对URI生成相对URI。比如,b/c
以https://a.com/b
为参数调用resolve()
将得到https://a.com/b/c
;https://a.com/b/c
以https://a.com/b
为参数调用relativize()
将得到c
。
public String getSchemeSpecificPart();
public String getAuthority();
public String getUserInfo();
public String getHost();
public int getPort();
...
-
URL
URL
类提供了从给定url获得资源的方法。URL
提供了与URI
类似的解析标识符的方法,此外还可以通过调用openStream()
获得InputStream
对象来获取资源内容。不过URL
类没有提供输出的方法,想要和服务器进一步交互,可以调用openConnection()
方法来获得URLConnection
对象来进一步操作。
URL url = new URL(urlString);
InputStream in = url.openStream();
-
URLConnection
该类提供了向服务器输出和接收服务器输入的功能。操作URLConnection
需要遵循一下几个步骤:
⑴ 调用URL
的openConnection()
来获得URLConnection
对象。
⑵ 设置参数和请求的属性。
⑶ 调用connect()
连接远程资源。
⑷ 建立连接后就可以查询头信息和访问资源数据。
- 参数和请求头属性
该类提供了一下方法设置参数和请求头属性(比如http header
),setter
都有对应的getter
。默认情况下,建立的连接只允许从服务器接收输入流,如果想要向服务器发送数据,需要先设置setDoOutput(true)
。修改请求头属性使用了键值对来进行修改,如果一个键可以有多个对应的值,则将值用list包装即可。
public void setAllowUserInteraction();
public void setDoInput();
public void setDoOutput();
public void setIfModifiedSince();//连接只对某个特定日期以来修改过的数据感兴趣
public void setUseCaches();
//修改请求头属性
public void addRequestProperty(String key, String value);
- 建立连接后的查询访问
建立连接后,该类还提供了如下方法进行头部的信息查询。
public Object getContent();
public String getHeaderField();
public InputStream getInputStream();
public OutputStream getOutputStream();
public String getContentEncoding();
public int getContentLength();
public String getContentType();
public long getDate();
public long getExpiration();
public long getLastModifed();
-
提交表单
想服务器提交表单有Get和Post两种方法。
- Get
对于Get方法来说,只需要遵循下面的规则将参数附在URL结尾处。
⑴ 保留字符A-Z、a-z、0-9、.、-、~、_。
⑵ 用+字符替换所有空格。
⑶ 将其他所有字符编码为UTF-8,并将每个字节都编码为%后面紧跟一个两位的十六进制数字。
例如San Francisco,CA
,可以使用San+Francisco%2+CA
。 - Post
Post方法将参数写入到输出流中,不在URL上显示长串的参数键值对。
URL url=new URL(urlString);
URLConnection connection=url.openConnection();
connection.setDoOutput(true);
PrintWriter out=new PrintWriter(connection.getOutputStream(),"utf-8");
out.print(name1+"="+URLEncoder.encode(value1,"utf-8")+"&");
out.print(name2+"="+URLEncoder.encode(value2,"utf-8"));
out.close();
- 重定向
服务器程序相应可能会产生重定向,重定向一般都是自动处理的,但是有些情况下,比如http与https之间的重定向因为安全原因不被支持,所以需要自己手动进行重定向。下面是手动处理重定向的步骤。
//在连接前关闭自动重定向
connection.setInstanceFollowRedirects(false);
//在发送请求之后获取相应码
int responseCode=connection.getResponseCode();
//如果是HttpURLConnection.HTTP_MOVED_PERM
//或HttpURLConnection.HTTP_MOVED_TEMP
//或HttpURLConnection.HTTP_SEE_OTHER
//就获取Location响应头,获得重定向URL,断开连接,创建到新的URL连接
String location=connection.getHeaderField("Location");
if(location!=null){
URL base=connection.getURL();
connection.disconnect();
connection=(HttpURLConnection) new URL(base,location).openConnection();
}
HTTP
HTTP有0.9、1.0、1.1、2.0这几个版本。
- HTTP 0.9
HTTP 0.9是第一个HTTP协议的版本,只支持GET请求,不支持请求头且没有协议头,只支持纯文本内容。该版本的协议就已经支持了无状态特性,事务结束就关闭请求,访问地址不存在也不会返回错误信息。 - HTTP 1.0
HTTP 1.0在0.9的版本上支持了请求和响应头部、响应以一个响应状态行开始、响应信息不仅限于超文本、支持了POST请求。该版本默认使用了短连接,每次请求建立一个TCP连接。 - HTTP 1.1
新增了keep alive、chunked编码传输、字节范围请求、请求流水线等特性。还支持了HOST域(即一个IP下可能有多个服务器,通过主机名指定)、增加了connect/trace/delete/put/options的请求方法。
- keep alive
该版本的HTTP协议默认使用长连接,即事务完成后不断开连接,除非客户端或服务器主动断开。 - chunked编码传输
该特性使得传输对象可以分块传输并标明长度,当长度表明为0时说明传输结束。 - 字节范围请求
当客户端已有请求对象的一部分内容时,可以向服务器请求其余部分的内容,而不用重新传输整个对象。
- HTTP 2.0
该协议是下一代HTTP协议,以下是其特性。
- 多路复用,将HTTP 1.1的包分为两帧,头部放在header帧,数据放在data帧,以实现加大带宽,降低延迟的目的。
- 所有通信在一个连接上完成,该连接可以承载任意数量的双向传输,每个消息被分成多帧传输,到达后根据头部的信息进行重新组装。
- 头部压缩,当一个客户端向同一服务器请求时,数据包的头部是相似的,HTTP 2.0提供了压缩的方法。
- 随时复位,HTTP 2.0可以随时停止一个TCP连接的传输,在不中断的情况下传输新的内容。
- 服务器端推流,客户端向服务器请求一个资源,服务器判断该客户端可能用到的其他资源,一并发送给客户端,客户端进行缓存。
- 优先权和依赖,可以指明哪些资源优先级更高,可以更快得到处理。
HTTP包结构
一个HTTP包由一下部分构成:起始行、一个或多个头域、一个空行表示头域的结束、实体消息。
GET http://download.microtool.de:80/somedata.exe
Host: download.microtool.de
Accept:*/*
Pragma: no-cache
Cache-Control: no-cache
Referer: http://download.microtool.de/
User-Agent:Mozilla/4.04[en](Win95;I;Nav)
Range:bytes=554554-
HTTP包的第一行为起始行,请求包的起始行包含请求方法、URL、HTTP版本;响应包的起始行包含HTTP-Version Status-Code Reason-Phrase
,HTTP-Version
表示支持的HTTP版本、Status-Code
表示三位数字的结果代码、Reason-Phrase
对结果代码的文本描述等。
上述请求包中Host
、Accept
、Pragma
就为头域,通过<key>:<value>
的形式表示。
头域可以分为通用头域、请求头域、响应头域和实体头域。
- 通用头域:
Date
表示消息发送的时间、Pragma
实现特定的指令比如Pragma:no-cache
、Connection
表示连接状态等。 - 请求头域:
Host
表示请求资源的URL、Accept
表示自己接收什么文件、Accept-Charset
表示接收文件的编码格式、Authorization
用来将身份验证信息发给服务器等。 - 响应头域:
Age
、Location
、Proxy-Authenticate
、Public
等。 - 实体头域:请求消息和响应消息都可以包含实体信息,实体信息一般由实体头域和实体组成,
Content-Type
实体的介质类型、Content-Length
表示实际传送的实体长度等。