Java网络编程

网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。

java.net 包中 J2SE的API包含有类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。

java.net 包中提供了两种常见的网络协议的支持:

  • TCP:TCP 是传输控制协议的缩写,它保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP / IP。
  • UDP:UDP 是用户数据报协议的缩写,一个无连接的协议。提供了应用程序之间要发送的数据的数据包。

在正式开始网络编程之前我们需要先了解一下几个概念:

开胃菜

URL & URI

URI: 统一资源标识符(Uniform Resource Identifier)用于标识某一个互联网资源名称的字符串。
包含:主机名、标识符、相对URI

URL: 统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理他

在Java中,URI表示一个统一资源的标识符,不能用于定位任何资源,唯一的作用就是解析。而URL则包含一个可以打开到达改资源的输入流,可以简单的理解URL是URI的一个特例。

URL

URL可以分为如下几部分

1
protocol://host:port/path

protocol(协议)可以是 HTTP、HTTPS、FTP 和 File,port 为端口号,path为文件路径及文件名。

HTTP 协议的 URL 实例如下:

1
http://gyl-coder.top/index.html

URL 解析:

  • 协议为(protocol):http
  • 主机为(host:port):gyl-coder.top
  • 端口号为(port): 80,以上URL实例并未指定端口,因为 HTTP 协议默认的端口号为 80。
  • 文件路径为(path):/index.html
URL方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public URL(String protocol, String host, int port, String file) throws MalformedURLException.
通过给定的参数(协议、主机名、端口号、文件名)创建URL。

public URL(String protocol, String host, String file) throws MalformedURLException
使用指定的协议、主机名、文件名创建URL,端口使用协议的默认端口。

public URL(String url) throws MalformedURLException
通过给定的URL字符串创建URL

public URL(URL context, String url) throws MalformedURLException
使用基地址和相对URL创建


public String getPath()
返回URL路径部分。

public String getQuery()
返回URL查询部分。

public String getAuthority()
获取此 URL 的授权部分。

public int getPort()
返回URL端口部分

public int getDefaultPort()
返回协议的默认端口号。

public String getProtocol()
返回URL的协议

public String getHost()
返回URL的主机

public String getFile()
返回URL文件名部分

public String getRef()
获取此 URL 的锚点(也称为"引用")。

public URLConnection openConnection() throws IOException
打开一个URL连接,并运行客户端访问资源。

下面通过代码来看具体操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.net.*;
import java.io.*;

public class Main {
public static void main(String [] args) {
try {
URL url = new URL("https://gyl-coder.top/");
System.out.println("URL 为:" + url.toString());
System.out.println("协议为:" + url.getProtocol());
System.out.println("验证信息:" + url.getAuthority());
System.out.println("文件名及请求参数:" + url.getFile());
System.out.println("主机名:" + url.getHost());
System.out.println("路径:" + url.getPath());
System.out.println("端口:" + url.getPort());
System.out.println("默认端口:" + url.getDefaultPort());
System.out.println("请求参数:" + url.getQuery());
System.out.println("定位位置:" + url.getRef());
}catch(IOException e) {
e.printStackTrace();
}
}
}

URL 为:https://gyl-coder.top/
协议为:https
验证信息:gyl-coder.top
文件名及请求参数:/
主机名:gyl-coder.top
路径:/
端口:-1
默认端口:443
请求参数:null
定位位置:null

URLConnections 类方法

RLConnection 方法列表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Object getContent() 
检索URL链接内容

Object getContent(Class[] classes)
检索URL链接内容

String getContentEncoding()
返回头部 content-encoding 字段值。

int getContentLength()
返回头部 content-length字段值

String getContentType()
返回头部 content-type 字段值

int getLastModified()
返回头部 last-modified 字段值。

long getExpiration()
返回头部 expires 字段值。

long getIfModifiedSince()
返回对象的 ifModifiedSince 字段值。

public void setDoInput(boolean input)
URL 连接可用于输入和/或输出。如果打算使用 URL 连接进行输入,则将 DoInput 标志设置为 true;如果不打算使用,则设置为 false。默认值为 true

public void setDoOutput(boolean output)
URL 连接可用于输入和/或输出。如果打算使用 URL 连接进行输出,则将 DoOutput 标志设置为 true;如果不打算使用,则设置为 false。默认值为 false

public InputStream getInputStream() throws IOException
返回URL的输入流,用于读取资源

public OutputStream getOutputStream() throws IOException
返回URL的输出流, 用于写入资源。

public URL getURL()
返回 URLConnection 对象连接的URL

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.net.*;
import java.io.*;

public class Main{

public static void main(String [] args){
try{
URL url = new URL("https://gyl-coder.top/");
URLConnection urlConnection = url.openConnection();
HttpURLConnection connection = null;
if(urlConnection instanceof HttpURLConnection){
connection = (HttpURLConnection) urlConnection;
} else{
System.out.println("请输入 URL 地址");
return;
}
BufferedReader in = new BufferedReader(
new InputStreamReader(connection.getInputStream()));
String urlString = "";
String current;
while((current = in.readLine()) != null){
urlString += current;
}
System.out.println(urlString);
}catch(IOException e){
e.printStackTrace();
}
}
}

运行之后会返回该网页的html内容

URI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
URI(String str) 
通过解析给定的字符串构造一个 URI。
URI(String scheme, String ssp, String fragment)
根据给定的组成部分构造 URI。
URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment)
根据给定的组成部分构造一个分层 URI。
URI(String scheme, String host, String path, String fragment)
根据给定的组成部分构造分层 URI。
URI(String scheme, String authority, String path, String query, String fragment)
根据给定的组成部分构造分层 URI。

int compareTo(URI that)
将此 URI 与另一个对象(也必须是 URI)进行比较。
static URI create(String str)
通过解析给定的字符串创建 URI。
boolean equals(Object ob)
测试此 URI 与另一对象的相等性。
String getAuthority()
返回此 URI 的已解码的授权组成部分。
String getFragment()
返回此 URI 的已解码的片段组成部分。
String getHost()
返回此 URI 的主机组成部分。
String getPath()
返回此 URI 的已解码的路径组成部分。
int getPort()
返回此 URI 的端口号。
String getQuery()
返回此 URI 的已解码的查询组成部分。
String getRawAuthority()
返回此 URI 的原始授权组成部分。
String getRawFragment()
返回此 URI 的原始片段组成部分。
String getRawPath()
返回此 URI 的原始路径组成部分。
String getRawQuery()
返回此 URI 的原始查询组成部分。
String getRawSchemeSpecificPart()
返回此 URI 原始的、特定于方案的部分。
String getRawUserInfo()
返回此 URI 的原始用户信息组成部分。
String getScheme()
返回此 URI 的方案组成部分。
String getSchemeSpecificPart()
返回此 URI 的特定于方案的解码部分。
String getUserInfo()
返回此 URI 的已解码的用户信息组成部分。
int hashCode()
返回此 URI 的哈希码值。
boolean isAbsolute()
判断此 URI 是否为绝对的。
boolean isOpaque()
判断此 URI 是否为不透明的。
URI normalize()
规范化此 URI 的路径。
URI parseServerAuthority()
尝试将此 URI 的授权组成部分(如果已定义)解析为用户信息、主机和端口组成部分。
URI relativize(URI uri)
根据此 URI 将给定 URI 相对化。
URI resolve(String str)
解析给定的字符串,然后在此 URI 的基础上构造一个新的 URI。
URI resolve(URI uri)
根据此 URI 解析给定的 URI。
String toASCIIString()
以 US-ASCII 字符串形式返回此 URI 的内容。
String toString()
以字符串形式返回此 URI 的内容。
URL toURL()
根据此 URI 构造一个 URL。

TCP协议和UDP协议

  TCP和UDP位于同一层,都是建立在IP层的基础之上。由于两台电脑之间有不同的IP地址,因此两台电脑就可以区分开来,也就可以互相通话了。通话一般有两种通话方式:第一种是TCP,第二种是UDP。TCP是可靠的连接,TCP就像打电话,需要先打通对方电话,等待对方有回应后才会跟对方继续说话,也就是一定要确认可以发信息以后才会把信息发出去。TCP上传任何东西都是可靠的,只要两台机器上建立起了连接,在本机上发送的数据就一定能传到对方的机器上,UDP就好比发电报,发出去就完事了,对方有没有接收到它都不管,所以UDP是不可靠的。TCP传送数据虽然可靠,但传送得比较慢,UDP传送数据不可靠,但是传送得快。

下面我们正式来学习网络编程

Socket套接字

一般的网络编程都称为Socket编程,Socket的英文意思是“插座”。

网络上具有唯一标识的IP地址和端口组合在一起才能构成唯一能识别的标识符套接字。

因为通信是相对的,我说你是我的Server只是从逻辑意义上来讲,我应该把东西先发到你那里去,然后由你来处理,转发。所以你叫Server。但从技术意义上来讲,只有TCP才会分Server和Client。对于UDP来说,从严格意义上来讲,并没有所谓的Server和Client。TCP的Server的插座就叫ServerSocket,Client的插座就叫Socket。

两台计算机互相连接,那么首先必须得知道它们的IP地址,但是只提供IP地址是不够的,还必须要有连接的端口号,也就是要连接到哪个应用程序上。

  端口号是用来区分一台机器上不同的应用程序的。端口号在计算机内部是占2个字节。一台机器上最多有65536个端口号。一个应用程序可以占用多个端口号。端口号如果被一个应用程序占用了,那么其他的应用程序就无法再使用这个端口号了。记住一点,我们编写的程序要占用端口号的话占用1024以上的端口号,1024以下的端口号不要去占用,因为系统有可能会随时征用。端口号本身又分为TCP端口和UDP端口,TCP的8888端口和UDP的8888端口是完全不同的两个端口。TCP端口和UDP端口都有65536个。   

Socket原理机制:
通信的两端都有Socket
网络通信其实就是Socket间的通信
数据在两个Socket间通过IO传输

Java中的网络支持:
针对网络通信的不同层次,Java提供了不同的API,其提供的网络功能有四大类:
InetAddress:用于标识网络上的硬件资源,主要是IP地址
URL:统一资源定位符,通过URL可以直接读取或写入网络上的数据
Sockets:使用TCP协议实现的网络通信Socket相关的类
Datagram:使用UDP协议,将数据保存在用户数据报中,通过网络进行通信。

InetAddress

InetAddress类用于标识网络上的硬件资源,标识互联网协议(IP)地址。 该类没有构造方法
使用方法:

1
2
3
4
5
6
7
8
9
//获取本机的InetAddress实例
InetAddress address =InetAddress.getLocalHost();
address.getHostName();//获取计算机名
address.getHostAddress();//获取IP地址
byte[] bytes = address.getAddress();//获取字节数组形式的IP地址,以点分隔的四部分

//获取其他主机的InetAddress实例
InetAddress address2 =InetAddress.getByName("其他主机名");
InetAddress address3 =InetAddress.getByName("IP地址");

TCP编程

TCP协议是面向连接的、可靠的、有序的、以字节流的方式发送数据,通过三次握手方式建立连接,形成传输数据的通道,在连接中进行大量数据的传输,效率会稍低

Java中基于TCP协议实现网络通信的类,客户端的Socket,服务器端的ServerSocket类

网络编程.png

Socket通信的步骤:

  1. 创建ServerSocket和Socket
  2. 打开连接到Socket的输入/输出流
  3. 按照协议对Socket进行读/写操作
  4. 关闭输入输出流、关闭Socket

服务器端:

  1. 创建ServerSocket对象,绑定监听端口
  2. 通过accept()方法监听客户端请求
  3. 连接建立后,通过输入流读取客户端发送的请求信息
  4. 通过输出流向客户端发送乡音信息
  5. 关闭相关资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 基于TCP协议的Socket通信,实现用户登录,服务端
*/
//1、创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket =newServerSocket(10086);//1024-65535的某个端口
//2、调用accept()方法开始监听,等待客户端的连接
Socket socket = serverSocket.accept();
//3、获取输入流,并读取客户端信息
InputStream is = socket.getInputStream();
InputStreamReader isr =newInputStreamReader(is);
BufferedReader br =newBufferedReader(isr);
String info =null;
while((info=br.readLine())!=null){
System.out.println("我是服务器,客户端说:"+info);
}
socket.shutdownInput();//关闭输入流
//4、获取输出流,响应客户端的请求
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);
pw.write("欢迎您!");
pw.flush();


//5、关闭资源
pw.close();
os.close();
br.close();
isr.close();
is.close();
socket.close();
serverSocket.close();

客户端:

  1. 创建Socket对象,指明需要连接的服务器的地址和端口号
  2. 连接建立后,通过输出流想服务器端发送请求信息
  3. 通过输入流获取服务器响应的信息
  4. 关闭响应资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//客户端
//1、创建客户端Socket,指定服务器地址和端口
Socket socket =newSocket("localhost",10086);
//2、获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream();//字节输出流
PrintWriter pw =newPrintWriter(os);//将输出流包装成打印流
pw.write("用户名:admin;密码:123");
pw.flush();
socket.shutdownOutput();
//3、获取输入流,并读取服务器端的响应信息
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String info = null;
while((info=br.readLine())!null){
System.out.println("我是客户端,服务器说:"+info);
}

//4、关闭资源
br.close();
is.close();
pw.close();
os.close();
socket.close();

应用多线程实现服务器与多客户端之间的通信:

  1. 服务器端创建ServerSocket,循环调用accept()等待客户端连接
  2. 客户端创建一个socket并请求和服务器端连接
  3. 服务器端接受苦读段请求,创建socket与该客户建立专线连接
  4. 建立连接的两个socket在一个单独的线程上对话
  5. 服务器端继续等待新的连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//服务器线程处理
//和本线程相关的socket
Socket socket =null;
//
public serverThread(Socket socket){
this.socket = socket;
}

publicvoid run(){
//服务器处理代码
}

//============================================
//服务器代码
ServerSocket serverSocket =newServerSocket(10086);
Socket socket =null;
int count =0;//记录客户端的数量
while(true){
socket = serverScoket.accept();
ServerThread serverThread =newServerThread(socket);
serverThread.start();
count++;
System.out.println("客户端连接的数量:"+count);
}

UDP编程

UDP协议(用户数据报协议)是无连接的、不可靠的、无序的,速度快;进行数据传输时,首先将要传输的数据定义成数据报(Datagram),大小限制在64k,在数据报中指明数据索要达到的Socket(主机地址和端口号),然后再将数据报发送出去
DatagramPacket类:表示数据报包
DatagramSocket类:进行端到端通信的类

服务器端实现步骤:

  1. 创建DatagramSocket,指定端口号
  2. 创建DatagramPacket
  3. 接受客户端发送的数据信息
  4. 读取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//服务器端,实现基于UDP的用户登录
//1、创建服务器端DatagramSocket,指定端口
DatagramSocket socket =new datagramSocket(10010);
//2、创建数据报,用于接受客户端发送的数据
byte[] data =newbyte[1024];//
DatagramPacket packet =newDatagramPacket(data,data.length);
//3、接受客户端发送的数据
socket.receive(packet);//此方法在接受数据报之前会一致阻塞
//4、读取数据
String info =newString(data,o,data.length);
System.out.println("我是服务器,客户端告诉我"+info);


//=========================================================
//向客户端响应数据
//1、定义客户端的地址、端口号、数据
InetAddress address = packet.getAddress();
int port = packet.getPort();
byte[] data2 = "欢迎您!".geyBytes();
//2、创建数据报,包含响应的数据信息
DatagramPacket packet2 = new DatagramPacket(data2,data2.length,address,port);
//3、响应客户端
socket.send(packet2);
//4、关闭资源
socket.close();

客户端实现步骤:

  1. 定义发送信息
  2. 创建DatagramPacket,包含将要发送的信息
  3. 创建DatagramSocket
  4. 发送数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//客户端
//1、定义服务器的地址、端口号、数据
InetAddress address =InetAddress.getByName("localhost");
int port =10010;
byte[] data ="用户名:admin;密码:123".getBytes();
//2、创建数据报,包含发送的数据信息
DatagramPacket packet = newDatagramPacket(data,data,length,address,port);
//3、创建DatagramSocket对象
DatagramSocket socket =newDatagramSocket();
//4、向服务器发送数据
socket.send(packet);


//接受服务器端响应数据
//======================================
//1、创建数据报,用于接受服务器端响应数据
byte[] data2 = new byte[1024];
DatagramPacket packet2 = new DatagramPacket(data2,data2.length);
//2、接受服务器响应的数据
socket.receive(packet2);
String raply = new String(data2,0,packet2.getLenth());
System.out.println("我是客户端,服务器说:"+reply);
//4、关闭资源
socket.close();

1、多线程的优先级问题:
根据实际的经验,适当的降低优先级,否侧可能会有程序运行效率低的情况
2、是否关闭输出流和输入流:
对于同一个socket,如果关闭了输出流,则与该输出流关联的socket也会被关闭,所以一般不用关闭流,直接关闭socket即可
3、使用TCP通信传输对象,IO中序列化部分
4、socket编程传递文件,IO流部分