首先来思考几个问题:
接下来以此篇文章来学习:
会涉及到的重难点:
此点介绍如何用Java访问外部资源,尤其是Java的URL对象来访问网络资源。
(1)网络基础知识:
IPv4地址(32位,4个字节),如:116.111.136.3;166.111.52.80
Ipv6地址(128位,16字节)
主机名(hostname),如:www.baidu.com
比如 116.111.136.3,就是一个IP地址,随着互联网资源越来越多,原有的地址已经无法表示那么多资源,继而出现了Ipv6地址,它能够表示的地址范围更加宽广。又因为IP地址不容易记住,互联网出现一套机制,即主机名也叫域名,例如 www.baidu.com,它容易记住,背后也对应着一个IP地址。
端口号(port number),如:80,21,1~1024位保留端口号
服务类型(service):如http、telnet、ftp、smtp
当我们一台服务器或者一个IP地址在提供服务的时候,它可以提供多种服务,并且每种服务通过端口号来区分,例如我们通常访问外部资源使用的是80端口,进行ftp时使用的是21端口,发送邮件使用的是25端口。在整个端口号当中,1~1024为保留端口,一般在进行开发时最好不要使用这些端口。这个所谓的端口号就像是生活中银行的窗口,每个窗口为我们提供不同的服务,而这家银行就相当于服务器,在服务器中提供着多类型的服务,例如http、telnet、ftp。
了解以上网络基础概念后,思考如何获取互联网上的信息,举个例子抓取新浪网上的新闻信息,查看以下代码:
import java.io.*;
import java.net.*;
public class URLReader {
public static void main(String args[]) throws Exception{
URL url = new URL("http://www.sina.com/");
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
System.out.print(inputLine);
in.close();
}
}
以上类URLReader 功能为网络资源的读取器,首先传入域名来创建URL对象来表示网络资源,接着构造一个缓冲输入流,需要在其构造方法中传入参数,此参数就是URL对象的openStream方法返回的资源流,这样缓冲输入流就可获取到资源信息,接着一行行去读取并输出来。
(2)URL类(Uniform Resource Locator)定义
一切资源定位器的简称,它表示Internet上某一资源的地址。
(3)URL的组成:protocol:resourceName
协议名指明获取资源所使用的传输协议,如http、ftp、gopher、file等,资源名则应该是资源的完整地址,包括主机名、端口号、文件名,甚至是文件内部的一个引子
(4)构造URL对象方法
URL urlBase = new URL("http://www.baidu.com");
URL gamelan = new URL("http://www.gamelan.com");
URL gamelanGames = new URL(gamelan, "Gamelan.game.html");
URL gamelanNetWork = new URL(gamelan, "Gamelan.net.html");
new URL("http", "www.gamelan.com", "/pages/Gamelan.net.html");
new URL("http", "www.gamelan.com", 80, "/pages/Gamelan.net.html");
最后需要注意的是创建URL对象时需要进行try catch处理,主要是预防MalformedURLException异常,因为用户给出的URL地址很可能是不符合规范的。
(5)获取URL对象属性
Tables | Are |
---|---|
public String getProtocol() | 返回URL对象采用的协议 |
public String getHost() | 返回URL对象的主机名 |
public String getPort() | 返回URL对象的端口号 |
public String getFile() | 返回URL对象采用的文件名 |
public String getRef() | 返回URL对象的引用地址 |
(1)定义
一个URLConnection对象代表一个URL资源与Java程序的通讯连接,可以通过它对这个URL资源读或写。
(2)URLConnection与URL的区别
(3)使用URLConnection通信的一般步骤(按顺序):
(4)实例展示
查看代码示例,通上个例子不同的是,此例采用URLConnection 对象来获取资源数据:
public class URLReader {
public static void main(String args[]){
try {
URL url = new URL("http://www.sina.com/");
URLConnection urlConnection = url.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
System.out.print(inputLine);
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
http中最常见的请求GET和POST可以来访问互联网上的资源,上节介绍的 URLConnection对象不仅可以获取资源,还可以发送一些信息到资源服务器上,将两者结合,首先来了解URLConnection编写GET请求:
(1)GET请求
public static String sendGet(String url, String param){
String result = "";
BufferedReader in = null;
try {
String urlName = url + "?" +param;
URL realUrl = new URL(urlName);
//打开和URL的连接
URLConnection urlConnection = realUrl.openConnection();
//设置通用的请求属性
urlConnection.setRequestProperty("accept", "*/*");
urlConnection.setRequestProperty("connection", "Keep-Alive");
//建立实际的连接
urlConnection.connect();
//定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭输入流
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
很显然,根据此方法的两个参数来构造最后的URL地址,根据URL对象获取到URLConnection对象,获取目标资源流并且设置了通用的请求属性,接着调用URLConnection对象的connect()
方法与URL建立了实际的连接,后面就是那个缓存输入流对象将读取的数据存储到字符串result中,最后返回。
(2)POST请求
POST方法就是由java程序向服务器发送请求的时候先发送一些参数,然后服务器再响应本地并返回相应内容,此过程(即POST请求的参数)需要通过URLConnection的输出流来写入参数,查看以下实例:
public static String sendPost(String url, String param){
PrintWriter out = null;
BufferedReader in = null;
String result = "";
try {
URL readUrl = new URL(url);
//打开与URL之间的连接
URLConnection urlConnection = readUrl.openConnection();
//设置通用的请求属性
urlConnection.setRequestProperty("accept", "*/*");
urlConnection.setRequestProperty("connection", "Keep-Alive");
//允许输出流
urlConnection.setDoOutput(true);
//获取URLConnection对象对应的输出流
out = new PrintWriter(urlConnection.getOutputStream());
//发送请求参数
out.print(param);
//flush输出流的缓冲
String line;
while ((line = in.readLine()) != null) {
result += line;
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭输入流
try {
if(out != null){
out.close();
}
if(in != null){
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
POST请求与GET请求不同处从允许URLConnection对象的输出流开始,借助PrintWriter类,传入URLConnection对象对应的输出流作为参数获取PrintWriter对象,发送请求参数到服务器。接着等待服务器返回数据流,随之处理并存储到字符串中返回出去。(后续处理逻辑相同,不赘述)
(3)总结
以上两个例子是Java中典型的GET和POST请求,通过此例子可学习使用URLConnection发送请求。总之,在URLConnection的基础上提供了一系列针对http请求的内容,对Java网络编程部分起着重要作用,例如以下:
Socket通信原理即如何在两个Java程序之间建立网络连接,再了解此之前先熟悉一下TCP传输协议。
(1)TCP(Transport Control Protocol)
面向连接的能够提供可靠的流式数据传输的协议。类似于生活中的打电话过程,通常拨完电话后会有一小段时间来建立连接,为了能够很好地进行语音传输。Java中有个类是使用TCP协议来进行网络通讯,例如:URL、URLConnection、Socket、ServerSocket。
(2)Socket通讯含义
网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个socket。socket通常用来实现客户端与服务端之间的连接。
查看上图,右侧的服务器可能提供多种类型的服务,例如http,通过端口80来提供服务。互联网上的用户通过网络连接到服务器上的http服务,而其它用户还可以连接到服务器的其它服务,例如SMTP(端口25),即发邮件的服务。
那么如何用Socket达到客户端和服务端连接的目的呢?
(3)Socket通讯原理
查看上图,客户端和服务器分别有一个Socket,由客户端向服务端发送一个Socket连接请求,服务器接收到后返回一个响应信号,这样两者之间建立了一个Socket连接。
上图是以代码的角度来讲解:在服务器有一个ServerSocket类的对象一直在运行着,等待客户端是否发起了请求,当它收到请求后ServerSocket会调用方法创建并返回一个Socket对象,此对象就是用来与客户端进行对等连接。当客户端和服务器建立Socket连接后,下一步就是传送数据,即通过Socket对象获取双方的输入输出流进行读写。
举个例子,当客户端发一个数据到服务器,接收到后再发送一个数据给客户端,此过程实际就是IO读写的方式,当网络建立起来后,所谓网络通讯就是IO读写。当两者皆完成了读写目的后,下一步则调用Socket的close
方法进行关闭,此过程结束。
接下来以代码实际例子来实现Socket通讯,而Java中正有一个类为Socket,来学习此类使用.
(1)Socket创建
Socket()
Socket(InetAddress address, int port)
InetAddress 代表需要构造Socket远程目标的连接地址,port则是连接目标对象的端口号。
Socket(String host, int port)
同上一个构造方法类似,只是主机名由字符串形式表示。
Socket(InetAddress host, int port, InetAddress localAddr, int localPort)
后两个参数代表本机的地址和端口号。
Socket(String host, int port, InetAddress localAddr, int localPort)
含义相同,只是第一个参数主机名由字符串形式表示。
(2)客户端与服务器的Socket创建
客户端Socket的建立
try{
Socket socket = new Socket("127.0.0.1", 2000);
}catch(IOException e){
System.out.println("Error:"+e);
}
创建Socket的第一个参数代表目标服务器的地址,而以上展示的 127.0.0.1 通常指本机,因为在调试程序时只有一台电脑,用它来同时启动两个虚拟机来互相连接,第二个参数是端口号,原则上不取1024以下的保留端口号即可,注意客户端与服务器端口号应一致。
服务器Socket的建立
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(2000);
}catch (Exception e){
System.out.print("can not listen to :" + e);
}
Socket socket = null;
try {
socket = serverSocket.accept();
}catch (Exception e){
System.out.print("Error :" + e);
}
可以看到服务端首先构造的并非是Socket 对象,而是ServerSocket 对象,传入构造方法中的参数就是端口号,同需连接客户端的端口号一致。接下来通过ServerSocket 对象的accept()
方法来获取Socket对象,此方法被称为阻塞方法,因为它一直在运行,等待客户端发送的Socket连接请求,若未收到请求,accept()
方法就一直在循环执行,始终不返回结果,直到收到请求后,accept()
方法会返回发送请求的Socket 对象。构造完客户端与服务端的Scocket对象后,接下来可以进行网络通讯了。
输入流与输出流
在通讯之前还需要利用Socket对象来构建输入输出流,代码如下所示:
【方式一:】
PrintStream outputStream = new PrintStream(new BufferedOutputStream(socket.getOutputStream()));
DataInputStream inputStream = new DataInputStream(socket.getInputStream());
【方式二:】
PrintWriter output = new PrintWriter(socket.getOutputStream(), true);
BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
输出流重点:通过Socket对象获取到输出流,先用BufferedOutputStream将其封装一层,外面再用PrintStream包一层,这样可以利用PrintStream来进行通讯。输出流的主要作用就是往外发送消息。
输入流重点:通过Socket对象获取到输出流,紧接着在外面包装一层DataInputStream ,从而获得DataInputStream 对象。输入流的主要作用就是接收来自另外一方的数据。方法二中的输入流,首先InputStreamReader 将字节流转换为字符流,BufferedReader 流能够读取文本行 , 通过向 BufferedReader 传递一个 Reader 对象 , 来创建一个 BufferedReader 对象 。
(3)简单聊天实例
以下以一个简单的例子——命令行聊天程序,来实践以上讲解的知识点。
客户端
【客户端 TalkClient】
public class TalkClient{
public static void main(String args[]) throws IOException {
Socket socket = new Socket("127.0.0.1", 4700);
BufferedReader inSystem = new BufferedReader(new InputStreamReader(System.in));
PrintWriter outputStream = new PrintWriter(socket.getOutputStream());
BufferedReader inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String readline = inSystem.readLine();
while (!readline.equals("bye")) {
outputStream.println(readline);
outputStream.flush();
System.out.println("Client:" + readline);
System.out.println("Server:" + inputStream.readLine());
readline = inSystem.readLine();
}
outputStream.close();
inputStream.close();
socket.close();
}
}
首先构造Socket对象,再依次构建三个流,分别是一个输入流来获取键盘输入,再构造一个输出流将信息发送给对方网络,最后构造一个输入流获取响应的信息。接着读取键盘输入,判断内容若不是“bye”则将信息发送给对方,再接收来自对方的信息,直到读取键盘输入“bye”时关闭输入输出流与socket连接。
服务端
【服务端 TalkServer 】
public class TalkServer {
public static void main(String args[]) throws IOException {
ServerSocket serverSocket = null;
Socket socket = null;
boolean listening = true;
try {
serverSocket = new ServerSocket(4700);
socket = serverSocket.accept();
} catch (Exception e) {
e.printStackTrace();
}
String line ;
BufferedReader inSystem = new BufferedReader(new InputStreamReader(System.in));
PrintWriter outputStream = new PrintWriter(socket.getOutputStream());
BufferedReader inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("Client"+ inputStream.readLine());
line = inSystem.readLine();
while (line.equals("bye")){
outputStream.println(line);
outputStream.flush();
System.out.println("Server"+ line);
System.out.println("Client"+ inputStream.readLine());
line = inSystem.readLine();
}
outputStream.close();
inputStream.close();
socket.close();
}
}
首先构造一个ServerSocket对象,通过服务端server的accept
方法来获取客户端发送的socket请求,该方法会返回socket对象,接下来需要站在服务端的角度进行通讯。还是照例构造三个流,含义不再赘述,接着后续逻辑与客户端相同。
(4)实例总结
服务端与客户端的例子很大部分内容相同,只是服务端需要构造一个ServerSocket,通过ServerSocket得到和客户端连接的Socket对象,得到对象后可以构造输入、出流进行IO流的读写,最后关闭流、Socket即可。所以网络编程到最后演化成UI编程,呈现出的效果:
Clent: hello!
Server:hey
Clent: how are you
Server:i am ok
...
Clent: bye
Server:bye
(最后需要注意的是想要实现以上效果,需先启动一个虚拟机运行服务端程序,再启动第二个虚拟机运行客户端程序。由于只是一个简单程序,所以两端之间只能一句一句互相说,可自行扩展)
下面会着重介绍多个程序之间进行通讯甚至是广播的知识点,首先来思考几个问题:
(1)多客户机制原理
通过一个例子来解释多客户机制,在服务端有一个ServerSocket一直在等待客户端发送请求。如图所示,ClientA发送请求到服务端,此时服务端实例化一个线程来处理与ClientA聊天相关的事,ClientB、ClientC也是如此。
(2)Socket多客户端例子
客户端
【客户端代码与第二节的简单聊天例子中的客户端TalkClient类完全相同,在此不重复贴】
服务端
【服务端 MultiTalkServer 】
public class MultiTalkServer {
static int clientNum = 0;
public static void main(String args[]) throws IOException {
ServerSocket serverSocket = null;
boolean listening = true;
try {
serverSocket = new ServerSocket(4700);
}catch (Exception e){
e.printStackTrace();
}
while (listening){
new ServerThread(serverSocket.accept(), clientNum).start();
clientNum++;
}
serverSocket.close();
}
public static class ServerThread extends Thread{
Socket socket = null;
int clientNum;
public ServerThread(Socket socket, int num) {
this.socket = socket;
clientNum = num + 1;
}
public void run(){
try {
String line ;
BufferedReader inSystem = new BufferedReader(new InputStreamReader(System.in));
PrintWriter outputStream = new PrintWriter(socket.getOutputStream());
BufferedReader inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("Client"+ clientNum + ":" + inputStream.readLine());
line = inSystem.readLine();
while (line.equals("bye")){
outputStream.println(line);
outputStream.flush();
System.out.println("Server"+ line);
System.out.println("Client"+ clientNum + ":" + inputStream.readLine());
line = inSystem.readLine();
}
outputStream.close();
inputStream.close();
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
服务端 类中有个静态成员变量clientNum用来记录发起请求的客户端数量,构造ServerSocket对象,在while循环方法体创建ServerThread线程。相当于main
方法一直在循环等待客户端socket请求,一旦接收到socket请求,就实例化一个线程与之交互聊天,线程记录数量加一。
来看ServerThread实现,内部维护了socket对象和客户端数量,run
方法中首先创建了三个流,意义与客户端相同,其实已经很是熟悉其中概念了,用一个输入流获取键盘输入,再用输出流将信息发送给对方网络,最后构造一个输入流获取响应的信息。同客户端相同,接下来判断键盘输入文本,为“bye”则停止发送,聊天结束;否则继续读取键盘输入流发送信息,最后关闭流和socket的连接。
注意:
(以上是展示的例子,如要运行查看效果,需先启动一个虚拟机运行服务端程序,再启动第二个虚拟机运行客户端程序,从而可以启动第三、四个去运行客户端程序,查看一个服务端同时与多个客户端进行聊天的效果)
此节将讲解数据报通信的原理,即不需要面向连线的通信方式,数据报通信方式采用的是UDP(User Datagram Protocol)协议,之前介绍过,这里同TCP作比较再次介绍。
(1)UDP与TCP学习
UDP(User Datagram Protocol)
非面向连接的提供不可靠的数据包式的数据传输协议。类似于从邮局发送信件的过程,发送信件是通过邮局系统一站一站进行传递,中间也有可能丢失。Java中有些类是基于UDP协议来进行网络通讯的,有DatagramPacket、DatagramSocket、MulticastSocket等类。
TCP(Transport Control Protocol)
面向连接的能够提供可靠的流式数据传输的协议。类似于打电话的过程,在拨完电话后两者之间会先建立连接,为了更好的通话,确保通话连接后两者开始互相传输信息。相对应的类有URL、URLConnection Socket、ServerSocket等
UDP 与 TCP的区别
TCP有建立时间,UDP无
(2)数据报学习
构造数据报的通信使用到的类
DatagramSocket实际上是一种数据报通信的Socket,构造它时可指定端口号。需要发送的数据存放在DatagramPacket对象中,第一种构造方法的第一个参数类型是字节数组,用来接收数据报,第二个参数即数据报的长度。第二种构造方法中的前两个参数意义相同,后两个个参数是指数据报被发送到的目标地址以及对应的端口号。
DatagramPacket packet = new DatagramPacket(buf, 256);
socket.receive(packet);
DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port);
socket.send(packet);
(3)数据报发送与接收实例
QuoteClient类主要目的是想服务器询问某些股票的信息,而服务端按照本地文件虚拟响应数据返回给客户端。
客户端
【客户端 QuoteClient 】
public class QuoteClient {
public static void main(String[] args)throws IOException{
if(args.length != 1){
System.out.println("Usage:java QuoteClient " );
return;
}
DatagramSocket socket = new DatagramSocket();
//发送请求
byte[] buf = new byte[256];
InetAddress address = InetAddress.getByName(args[0]);
DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 4445);
socket.send(packet);
//获取响应
packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
//显示响应数据
String received = new String(packet.getData());
System.out.println("Quote of the Moment:"+ received);
socket.close();
}
}
QuoteClient类中main
方法中第一行if判断参数args是否等于1,意味着程序在执行时必须跟一个参数,即目标服务器的主机名,若无会打印Usage:java QuoteClient
提示。接下来依次构造Socket对象和数据报 ,这个目标地址InetAddress就是main
方法中的参数args[0],再构造一个数据报发送。发送结束后想要接收到服务端的回信,即包含了客户端想要的股票信息。最后关闭socket连接。
服务端
【服务端 QuoteServer 】
public class QuoteServer {
public static void main(String[] args)throws IOException{
new QuoteServerThread().start();
}
public static class QuoteServerThread extends Thread {
protected DatagramSocket socket = null;
protected BufferedReader in = null;
protected boolean moreQuotes = true;
public QuoteServerThread() throws IOException{
this("QuoteServerThread");
}
public QuoteServerThread(String name) throws IOException{
super(name);
socket = new DatagramSocket(4445);
try{
in = new BufferedReader(new FileReader("one-lines.txt"));
}catch(FileNotFoundException e){
e.printStackTrace();
}
}
public void run(){
while(moreQuotes){
try{
byte[] buf = new byte[256];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String dString = null;
if(in == null){
dString = new Date().toString();
}else{
dString = getNextQuote();
}
buf = dString.getBytes();
//返回数据给客户端(address + port)
InetAddress address = packet.getAddress();
int port = packet.getPort();
packet = new DatagramPacket(buf, buf.length, address, port);
socket.send(packet);
}catch(Exception e){
e.printStackTrace();
moreQuotes = false;
}
}
socket.close();
}
private String getNextQuote() {
String returnValue = null;
try{
if((returnValue = in.readLine()) == null){
in.close();
moreQuotes = false;
returnValue = "NO more quotes. Goodbye!";
}
}catch(Exception e){
e.printStackTrace();
}
return returnValue;
}
}
}
服务端QuoteServer类main
方法开启了一个线程,首先来看线程QuoteServerThread的构造方法,主要是创建Socket,构造一个文件输入流,因为这是在模仿服务端发送信息到客户端,所以将股票信息写到此文件中,这样每次有客户端发送请求咨询股票价格时,就从文件读出对应股票的价格返回给客户端。再看run
方法,首先一个While循环代表文件中信息未读完的话一直读取,在接收客户端发来的接收包后发送回信,判断服务端的文件内容是否为空,是则返回客户端当前日期,否则获取下一条股票信息将其返回。注意返回信息的前提是构造一个数据报包,需要知道客户端的地址和端口号,而正好利用接收到客户端发来的数据报包,通过数据报包对象来获取地址和端口号。
(4)例子总结
通过以上例子可以得出结论,整个过程非常类似于生活中互相写信的通讯方式。客户端构造一个DatagramPacket对象,填充一些信息,通过DatagramSocke对象t的send()
方法发送到服务端。服务端接收到数据报包后,同时也得知了客户端的地址和端口号,服务端也构造一个DatagramPacket数据报,填充响应的数据返回给客户端。
其实利用数据报包可进行广播通讯,只适用于小范围局域网。之前介绍的DatagramSocket 只允许存放一个目的地址,但是MulticastSocket 类可以把数据报以广播的形式发送到所有监听该端口的客户端。MulticastSocket 在客户端使用,来监听服务器广播来的数据。下面通过一个实例来学习:
【客户端 MulticastClient】
public class MulticastClient {
public static void main(String args[]) throws IOException{
MulticastSocket multicastSocket = new MulticastSocket(4446);
InetAddress address = InetAddress.getByName("230.0.0.1");
multicastSocket.joinGroup(address);
DatagramPacket packet;
//获取接收数据
for(int i=0; i<5; i++){
byte[] buf = new byte[255];
packet = new DatagramPacket(buf, buf.length);
multicastSocket.receive(packet);
String received = new String(packet.getData());
System.out.println("Quote of the Moment:"+received);
}
multicastSocket.leaveGroup(address);
multicastSocket.close();
}
}
首先构造一个广播socket对象,同时需要传入端口号到构造方法,接着构造一个IP地址,调用广播socket对象的joinGroup
方法将socket对象加入一个组,即IP地址所标识的组。接下来以for循环来模拟客户端接收广播信息的过程。最后循环5次接收数据后调用广播socket对象的 leaveGroup
的方法,即离开当初关注的那个组,然后关闭socket连接。
以上是Java网络编程着重于Socket的学习笔记,中间加述了大多例子来实践综合了Socket知识点,但这篇文章并不代表记录了全部的知识点,只是尽我所能来记录现有可掌握到的知识。如有错误,望指正~
希望对你们有帮助:)