Java基础:网络编程

网络通讯要素

IP地址(InetAddress)

  • 网络中设备的标识
  • 不易记忆,可用主机名代替
  • 本地地址:127.0.0.1
  • 主机名:localhost

端口号

  • 用于标识进程的逻辑地址,不同进程的标识
  • 有效端口为 2 个字节(0-65535),其中 0-1024 一般为系统使用或保留端口

传输协议

  • UDP(User Datagram Protocol):用户数据报协议
    • 将数据及源和目的封装成数据包中,不需要建立连接。
    • 每个数据报的大小限制在 64K 。
    • 因无连接,是不可靠协议。
    • 不需要建立连接,速度快。
  • TCP(Transmission Control Protocol):传输控制协议
    • 建立连接,然后形成传输数据的通道。
    • 在连接中可以进行大数据量传输。
    • 通过三次握手完成连接,是可靠协议。
    • 必须建立连接,效率会稍低。

Socket

  • Socket就是为网络服务提供的一种机制
  • 通信等两端都有 Socket
  • 网络通信其实就是 Socket 间的通信
  • 数据在两 个Socket 间通过 IO 传输。

IP

概述

  • IP 地址是 IP 使用的 32 位或 128 位无符号数字,它是一种低级协议,UDP 和 TCP 协议都是在它的基础上构建的。
  • InetAddress 类是表示互联网协议(IP)地址。
  • InetAddress 类没有提供构造方法,所以不能 new 创建对象,要通过静态方法来初始化。
    1
    2
    3
    4
    5
    static InetAddress[] getAllByName(String host): 根据系统上配置的名称服务返回其 IP 地址所组成的数组。
    static InetAddress getByAddress(byte[] addr): 在给定原始 IP 地址的情况下,返回 InetAddress 对象。
    static InetAddress getByAddress(String host, byte[] addr): 根据提供的主机名和 IP 地址创建 InetAddress。
    static InetAddress getByName(String host): 在给定主机名的情况下确定主机的 IP 地址。
    static InetAddress getLocalHost(): 返回本地主机。

常用方法演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressDemo {
public static void main(String[] args) throws UnknownHostException {
//获取本地主机
InetAddress ip = InetAddress.getLocalHost();

System.out.println("本地主机ip:" + ip.getHostAddress() + ",名称:" + ip.getHostName());

InetAddress ip2 = InetAddress.getByName("www.baidu.com");

System.out.println("百度主机ip:" + ip2.getHostAddress() + ",名称:" + ip2.getHostName());
}
}

UDP

概述

UDP 协议中有 DatagramSocket 和 DatagramPacket 对象,两者分别为,UDP 服务与 数据包。

UDP 发送端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.net.InetAddress;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
public class UDPSendDemo {
public static void main(String[] args) throws Exception {
//建立一个 UDP 服务
DatagramSocket ds = new DatagramSocket();

byte[] buf = "Hello!".getBytes();
//创建一个数据包,这里的参数分别代表字节数据,字节数据的长度,目标IP,目标端口
DatagramPacket dp = new DatagramPacket(buf, buf.length,
InetAddress.getByName("127.0.0.1"), 10001);

//发送数据
ds.send(dp);

//关闭资源
ds.close();
}
}

因为这里是数据发送端,所以创建的数据包中要指定目标IP与端口。

UDP 接收端

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
import java.net.InetAddress;
import java.net.DatagramSocket;
import java.net.DatagramPacket;

public class UDPReceiveDemo {
public static void main (String[] args) throws Exception {
//建立一个UDP服务,并监听发送端发送数据的端口。
DatagramSocket ds = new DatagramSocket(10001);

//创建接收数据的数据包
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);

//将接收到的数据放到指定数据包中。
ds.receive(dp);

//解析并打印接收到的数据
String message = new String(dp.getData(), 0, dp.getLength());
String ip = dp.getAddress().getHostAddress();
int port = dp.getPort();

System.out.println(ip + "::" + port + "---" + message);
ds.close();
}
}

接收端需要注意的是:

  • 服务端要监听发送端发送的数据包中的那个端口,才能收到数据。
  • DatagramSocketreceive 方法是阻塞式方法,当未接收到数据包时,会一直在等待接收状态。

模拟自动聊天功能

需求:模拟出一个与机器人自动聊天的功能,给机器人发送字符数据,机器人将字符数据的大写反馈给用户,要求用户使用键盘录入数据,并可循环发送数据,直到用户发送 over。

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;

class Send implements Runnable {

@Override
public void run() {
DatagramSocket ds = null;
System.out.println("客户端已开启");
try {
ds = new DatagramSocket();

//输入流是键盘录入
BufferedReader bur = new BufferedReader(new InputStreamReader(System.in));

String line = null;

while ((line = bur.readLine()) != null) {
if ("over".equals(line)) {
System.out.println("本客户端已退出");
break;
}

DatagramPacket dp = new DatagramPacket(line.getBytes(),
line.length(), InetAddress.getByName("127.0.0.1"), 10005);

ds.send(dp);
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ds != null)
ds.close();
}
}
}

class Receive implements Runnable {

@Override
public void run() {
System.out.println("服务端已开启");
DatagramSocket ds = null;
try {
ds = new DatagramSocket(10005);
while (true) {
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);

ds.receive(dp);

String message = new String(dp.getData(), 0, dp.getLength());
String ip = dp.getAddress().getHostAddress();
int port = dp.getPort();

System.out.println(ip + "::" + port + "---" + message.toUpperCase());
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ds != null)
ds.close();
}
}
}

public class ThreadUDP {
public static void main(String[] args) {
Thread send = new Thread(new Send());
Thread receive = new Thread(new Receive());

receive.start();
send.start();
}
}

TCP

概述

在 TCP 协议中的客户端为:Socket,服务端为:ServerSocket,传输时使用的是字节数据。

TCP 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.net.Socket;
import java.io.OutputStream;

public class TCPClient {
public static void main(String[] args) throws Exception {
//创建客户端的 Socket 服务,指定目的和端口
Socket s = new Socket("127.0.0.1", 10030);

//为了发送数据,获取 Socket 流中的输出流
OutputStream out = s.getOutputStream();

byte[] buf = "Hello,TCP".getBytes();
out.write(buf);

s.close();
}
}

因为 TCP 是面向连接的,所以不能单独运行客户端,需要先启动服务端在启动客户端,然后发送数据过去。

TCP 服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.net.Socket;
import java.net.ServerSocket;
import java.io.InputStream;

public class TCPServer {
public static void main(String[] args) throws Exception {
//创建服务端的 Socket 服务,并监听一个用来接收数据的端口。
ServerSocket ss = new ServerSocket(10030);

Socket s = ss.accept();
InputStream is = s.getInputStream();

byte[] buf = new byte[1024];
int len = 0;
String message = "";
while ((len = is.read(buf)) != -1) {
message += new String(buf, 0, len);
}
System.out.println("message:" + message);

ss.close();
}
}
  • appept 方法也是一个阻塞式方法,在连接传入前会一直阻塞。
  • 使用 appept 方法的返回值可以得到发送端的 Socket 对象,用这个对象获取输入流,再用其读取数据。

模拟自动聊天功能

这里的功能与 UDP 的方式不同,因为是面向连接的,所以会更加复杂一些。

Socket 对象只能获取字节流,而不能获取字符流,所以我们需要使用转换流将字节流转化为字符流在进行操作。

客户端:

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
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;

public class TCPSocketClient {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("客户端已开启");

//定义目标客户端的 IP 和端口
Socket s = new Socket("127.0.0.1", 10080);

//用来从键盘读取用户要发送的数据
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

//用来将用户数据写入出去
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));

//用来读取服务端的反馈信息
BufferedReader read = new BufferedReader(new InputStreamReader(s.getInputStream()));

//从键盘读取数据,直到用户输入 over
String line = null;
while ((line = br.readLine()) != null) {
if ("over".equals(line)) {
break;
}
//将数据通过 Socket 流写出到服务端,由于是字符流,
//要给出结束标志"回车符",并刷新缓存,不然数据只会存储在内存中。
bw.write(line);
bw.newLine();
bw.flush();

//readLine 是阻塞式方法,等待服务端的反馈信息。
String message = read.readLine();
System.out.println("从服务器接收到的反馈数据:" + message);

}
br.close();

//客户端关闭之前,会给服务端发送一个结束标志,这样不会导致链接直接中断
s.close();
}
}

服务端:

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
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPSocketServer2 {
public static void main(String[] args) throws IOException {
System.out.println("服务端已开启");
ServerSocket ss = new ServerSocket(10080);
Socket s = ss.accept();

String ip = s.getInetAddress().getHostAddress();
System.out.println(ip + "已连接");

//用来接收客户端发送的数据
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
//用来反馈给客户端
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line = null;
while ((line = br.readLine()) != null) {

//这里反馈数据也要加回车标记,并刷新。
bw.write(line.toUpperCase());
bw.newLine();
bw.flush();
}
ss.close();
}
}

这里要注意的是,使用转换流后,两端互发数据,务必要加回车符,并刷新缓存。

模拟上传文件

需求:模拟客户端向服务端上传一个文件,要求使用 TCP 协议,上传完成后,服务端给予反馈信息。
客户端:

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
import java.net.*;
import java.io.*;
public class UploadClient {
public static void main(String[] args) throws Exception{
Socket s = new Socket("127.0.0.1", 10030);

FileInputStream fis = new FileInputStream("E:\\demo.jpg");

OutputStream os = s.getOutputStream();

byte[] buf = new byte[1024];
int len = 0;
while ((len = fis.read(buf)) != -1) {
os.write(buf, 0, len);
}

s.shutdownOutput();

InputStream is = s.getInputStream();
byte[] bufIn = new byte[1024];
int lenIn = is.read(bufIn);
System.out.println(new String(bufIn, 0, lenIn));

fis.close();
s.close();

}
}

注意:客户端发送完文件后,要执行 s.shutdownOutput() 方法给予服务端发送结束标志,不然服务端会一直等待接收数据。

服务端:

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
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class UploadServer {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(10030);

Socket s = ss.accept();
InputStream is = s.getInputStream();

FileOutputStream fos = new FileOutputStream("F:\\server.jpg");

byte[] buf = new byte[1024];
int len = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
}

OutputStream os = s.getOutputStream();
os.write("上传成功!".getBytes());

fos.close();
s.close();
ss.close();
}
}

模拟并发上传图片

在上一个例子中,客户端一次只能给服务器发送一次文件,服务器也之只能接收一次文件后就关闭了,如果给服务器的 接收文件的代码块 加上 while 循环,虽然能解决问题,但是出现的新问题就是当一个客户端未上传完数据时,其他客户端只能处于等待状态。

所以要用多线程处理,每有一个新的客户端发送请求,服务端就创建一个新线程来处理该客户端的上传任务。

假定客户端上传的都是 jpg 格式的图片,服务端接收文件后的命名规则是 客户端ip(n).jpg,n 代表客户端上传文件的次数。

客户端代码不变!!!

多线程部分:

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
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class UploadThread implements Runnable {
private Socket s;


public UploadThread(Socket s) {
this.s = s;
}

@Override
public void run() {
String ip = s.getInetAddress().getHostAddress();
int count = 1;
try {
System.out.println(ip + "..connected");

InputStream is = s.getInputStream();

File file = new File(ip + "(" + count++ + ").jpg");

while (file.exists())
file = new File(ip + "(" + count++ + ").jpg");

FileOutputStream fos = new FileOutputStream(file);

byte[] buf = new byte[1024];
int len = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
}

OutputStream os = s.getOutputStream();
os.write("上传成功!".getBytes());
System.out.println("成功接收一张图片,存储位置" + file.getAbsolutePath());

fos.close();
s.close();
} catch (IOException e) {
new RuntimeException(ip + ":文件上传失败!");
}
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.net.ServerSocket;
import java.net.Socket;

public class UploadServer {
public static void main(String[] args) throws Exception {
System.out.println("服务器已开启!");
ServerSocket ss = new ServerSocket(10030);
while (true) {
Socket s = ss.accept();
new Thread(new UploadThread(s)).start();
}
}
}

  • 这里的命名规则是为了防止覆盖原文件,也可以用时间戳或其他不会出现重复的形式。

URL

URL 代表一个统一资源定位符,它是指向互联网“资源”的指针。

例如我们经常使用的百度:http://www.baidu.com

由于 URL 中包含了协议、主机地址、主机端口、请求路径、请求资源等信息。所以在 Java 中也是有相应的对象来表示的。

常用方法演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.net.MalformedURLException;
import java.net.URL;

public class URLDemo {
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("https://www.baidu.com/search/error.html?name=zhangsan&age=13");
System.out.println("getProtocol:" + url.getProtocol());
System.out.println("getHost:" + url.getHost());
System.out.println("getPort:" + url.getPort());
System.out.println("getPath:" + url.getPath());
System.out.println("getFile:" + url.getFile());
System.out.println("getQuery:" + url.getQuery());
}
}

运行结果

getProtocol:https
getHost:www.baidu.com
getPort:-1
getPath:/search/error.html
getFile:/search/error.html?name=zhangsan&age=13
getQuery:name=zhangsan&age=13

根据运行结果可以看到根据 URL 对象的方法可以很方便的获取到 URL 的各种信息,不用再手动的进行切割信息了。

如 URL 中未指定端口,那么返回的是 -1

获取响应信息

URL 对象的内部还对 Socket 对象进行封装,可以直接对某个 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.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;

public class URLDemo2 {
public static void main(String[] args) throws Exception {
URL url = new URL("https://www.baidu.com/search/error.html");

//连接指定 URL 地址
URLConnection conn = url.openConnection();

//获取响应头信息
System.out.println("getContentEncoding:" + conn.getContentEncoding());
System.out.println("getContentLength:" + conn.getContentLength());
System.out.println("getContentType:" + conn.getContentType());
System.out.println("getDate:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").
format(new Date(conn.getDate())));

//获取响应的数据
InputStream is = conn.getInputStream();
byte[] buf = new byte[1024];
int len = 0;
int size = 0;
while ((len = is.read(buf)) != -1) {
size += len;
System.out.println(new String(buf, 0, len,"utf-8"));
}

is.close();
}
}

Java 对网络编程这一块,封装的还是很好用的,需要使用更多的方法,可以去 API 文档上查看。

坚持原创技术分享,您的支持将鼓励我继续创作!