本文整理了学习Java网络通信编程的笔记,并分析了若干程序实例,以巩固学习成果。
1 网络通信编程笔记
1.1 网络程序设计基础
1.1.1 基本概念
- 局域网(LAN)、广域网(WAN)
- IP协议,IP地址(IPv4,4个字节表示)
- TCP协议(传输控制协议):类似拨打电话,固接连线,可靠性高,有顺序
- UDP协议(数据用户报协议):类似发送信件,无连接通信,可靠性低,不保证顺序
- 端口(port):假想的连接装置,计算机与网络的物理连接,为整数
- 套接字(Socket):假想的连接装置,连接程序与端口
1.1.2 网络通信的要素
- 通信双方地址:IP、端口号
- 网络通信协议:TCP/IP协议
1.2 TCP程序设计基础
1.2.1 InetAddress
类
与IP地址相关的类,注意该类会抛UnknownHostException
异常
- IP地址:
- 本机
localhost
(127.0.0.1)
- IPv4(4个字节组成,42亿),IPv6(128位,8个无符16进制整数)
- 公网(互联网),私网(局域网),ABCD类地址
- 无构造器,不可被
new
,只可被自己的方法返回
- 常用方法:
getByName(String host)
获取与Host对应的InetAddress
对象
getHostAddress()
获取对象所包含的IP地址,返回String
getHostName()
获取IP主机名,返回String
getLocalHost()
获取本地主机的InetAddress
对象
1.2.2 ServerSocket
类
服务器套接字,等待网络请求,注意该类会抛IOException
异常
- 端口:
- 0~65535
- 不同的进程用不同的端口号,类似于门牌
- 端口分类:
- 公有端口0~1023(
HTTP
80、HTTPS
443、FTP
21、Telent
23)
- 程序注册端口1024~49151(
Tomcat
8080、MySQL
3306、Oracle
1521)
- 动态(私有)端口49152~65535
InetSocketAddress
类:与InetAddress
类似,加入了端口,可以new
,传入String地址和int端口,有getPort()
等方法
ServerSocket
用于等待网络请求,构造方法:
ServerSocket()
非绑定服务器套接字
ServerSocket(int port)
绑定特定端口
ServerSocket(int port, int backlog)
指定本机端口、指定的backlog
ServerSocket(int port, int backlog, InetAddress bindAddress)
指定端口、侦听backlog
和绑定到的本地IP地址
ServerSocket
的常用方法:
accept()
等待客户机连接,若连接返回一个Socket套接字
isBound()
判断绑定状态
getInetAddress()
返回本地地址的InetAddress
isClosed()
返回关闭状态
close()
关闭服务器套接字
bind(SocketAddress endpoint)
绑定到特定地址(IP和端口)
getLocalPort()
获取等待端口
1.2.3 TCP网络程序
通信协议:速率、传输码率、代码结构、传输控制等
- TCP/IP协议:协议簇,最出名的是TCP协议和IP协议
- TCP:连接、稳定,三次握手四次挥手,客户端服务端架构,传输完成释放连接,效率低
- UDP:不连接、不稳定,客户端服务端无明确界限,效率高
参见第2章的实例
1.2.4 Tomcat基础
- Tomcat是一个服务端,客户端通过浏览器进入
- 一般使用8080端口
1.4 UDP程序设计基础
1.4.1 UDP通信
- 基本模式:
- 发送数据包步骤:
- 创建数据报套接字(
DatagramSocket()
)
- 创建发送的数据包(
DatagramPacket(byte[] buf, int offset, int length, InetAddress ip, int port)
)
- 发送数据包(
DatagramSocket().send()
)
- 接收数据包步骤:
- 创建数据报套接字并绑定到端口(
DatagramSocket(int port)
)
- 创建字节数组接收数据包(
DatagramPacket(byte[] buf, int length)
)
- 接收UDP包(
DatagramSocket().receive()
)
1.4.2 DatagramPacket
类
- 表示数据包
- 构造方法:
DatagramPacket(byte[] buf, int length)
指定数据包的内存空间和大小
DatagramPacket(byte[] buf, int length, InetAddress ip, int port)
指定数据包的目标地址和端口
1.4.3 DatagramSocket
类
- 表示发送和接收数据包的数据报套接字
- 构造方法:
DatagramSocket()
绑定到本地主机任何可用端口
DatagramSocket(int port)
绑定到本地主机指定端口
DatagramSocket(int port, InetAddress ip)
绑定到指定的本地地址
1.4.4 UDP网络程序
1.5 URL类
统一资源定位器,通过地址定位互联网上的资源
2 TCP网络程序示例
2.1 简单的接收器(服务端)、发送器(客户端)程序
接收器(MyReceiver.java
)
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
| import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket;
public class MyReceiver { public static void main(String[] args) { ServerSocket serverSocket = null; Socket socket = null; InputStream is = null; ByteArrayOutputStream baos = null;
try { serverSocket = new ServerSocket(9005); socket = serverSocket.accept(); is = socket.getInputStream(); baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { baos.write(buffer, 0, len); } System.out.println(baos); } catch (IOException e) { e.printStackTrace(); } finally { if (baos != null) { try { baos.close(); } catch (IOException e) { e.printStackTrace(); } } if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
|
发送器(MyTransmitter.java
)
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
| import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket;
public class MyTransmitter { public static void main(String[] args) { Socket socket = null; OutputStream os = null; try { InetAddress serverIP = InetAddress.getByName("127.0.0.1"); int port = 9005; socket = new Socket(serverIP, port); os = socket.getOutputStream(); os.write("你好!".getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { if (os != null) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } } if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
|
说明:
- 服务端和客户端都需要有套接字,分别是
ServerSocket
和Socket
,并且绑定到统一端口。服务端还有一个Socket
,是连接端口的返回结果。
- 注意输入流和输出流的使用,客户端使用输出流,通过套接字输出;服务端使用输入流,通过套接字输入,而这股输入流需要输出的话,还需要一个输出流。
先启动MyReceiver.java
,再启动MyTransmitter.java
,观察到MyReceiver.java
输出:
2.2 较复杂的服务端、客户端程序
服务端(MyTCPServer.java
)
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
| import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket;
public class MyTCPServer { private ServerSocket server; private Socket socket; private BufferedReader reader;
public static void main(String[] args) { MyTCPServer tcp = new MyTCPServer(); tcp.getServer(); }
void getServer() { try { server = new ServerSocket(8998); System.out.println("服务器套接字创建成功!"); while (true) { System.out.println("等待客户端连接..."); socket = server.accept(); System.out.println("客户端连接成功!"); reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); getClientMessage(); } } catch (IOException e) { e.printStackTrace(); } }
private void getClientMessage() { try { while (true) { System.out.println("客户端:" + reader.readLine()); } } catch (IOException e) { e.printStackTrace(); } try { if (reader != null) { reader.close(); } if (socket != null) { socket.close(); } if (server != null) { server.close(); } } catch (IOException e) { e.printStackTrace(); } } }
|
客户端(MyTCPClient.java
)
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
| import javax.swing.*; import javax.swing.border.BevelBorder; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.io.PrintWriter; import java.net.Socket;
public class MyTCPClient extends JFrame { Container c; private final JTextArea ta = new JTextArea(); private final JTextField tf = new JTextField(); Socket socket; private PrintWriter writer;
public MyTCPClient(String title) { super(title); setDefaultCloseOperation(EXIT_ON_CLOSE); c = getContentPane(); final JScrollPane sp = new JScrollPane(); sp.setBorder(new BevelBorder(BevelBorder.RAISED)); c.add(sp, BorderLayout.CENTER); sp.setViewportView(ta); ta.setFont(new Font("微软雅黑", Font.PLAIN, 16)); ta.setEditable(false); c.add(tf, BorderLayout.SOUTH); tf.setFont(new Font("微软雅黑", Font.PLAIN, 16)); tf.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { writer.println(tf.getText()); ta.append(tf.getText() + '\n'); ta.setSelectionEnd(ta.getText().length()); tf.setText(""); } }); }
public static void main(String[] args) { MyTCPClient client = new MyTCPClient("客户端系统"); client.setBounds(600, 300, 400, 400); client.setVisible(true); client.connect(); }
private void connect() { ta.append("尝试连接中...\n"); try { socket = new Socket("127.0.0.1", 8998); writer = new PrintWriter(socket.getOutputStream(), true); ta.append("完成连接!\n"); } catch (IOException e) { ta.append("连接失败!请检查服务器端\n"); } } }
|
说明:
- 服务端将获取客户端的方法放在无限循环中,以便无限接收客户端的信息。
- 根据屏幕提示的信息,确定客户端是何时连接上服务端的。
先启动MyTCPServer.java
,再启动MyTCPClient.java
,在窗体输入文字,观察到窗体变化与MyTCPServer.java
输出:
1 2 3 4 5
| 服务器套接字创建成功! 等待客户端连接... 客户端连接成功! 客户端:你好! 客户端:我是客户端
|
2.3 简单的文件传输程序
服务端(FileReceiver.java
)
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
| import java.io.*; import java.net.ServerSocket; import java.net.Socket;
public class FileReceiver { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(9000); Socket socket = server.accept(); InputStream is = socket.getInputStream(); FileOutputStream fos = new FileOutputStream("receive.png"); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); }
BufferedWriter endBw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); endBw.write("接收完毕");
endBw.close(); fos.close(); is.close(); socket.close(); server.close(); } }
|
客户端(FileSender.png
)
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.io.*; import java.net.Socket;
public class FileSender { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 9000); OutputStream os = socket.getOutputStream(); FileInputStream fis = new FileInputStream("send.png"); byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { os.write(buffer, 0, len); }
socket.shutdownOutput();
BufferedReader endBr = new BufferedReader(new InputStreamReader(socket.getInputStream())); System.out.println(endBr.readLine());
endBr.close(); fis.close(); os.close(); socket.close(); } }
|
说明:
- 示例中涉及到的输入输出流分别有(以输入流为例):
InputStream
(接收套接字流)、FileInputStream
(用于文件与系统间的传输流)、InputStreamReader
(管道)、BufferedReader
(用于处理服务端的回传字符)。
- 注意客户端第15行的
socket.shutdownOutput();
,若不写这一行,即使客户端已写入完毕(并开始进行输入流的等待),服务端仍在继续读取(因为服务端第13行的is.read(buffer)
,读取的字节数已为0,因此循环无法退出,处于读取0字节写入0字节的状态),因此要单向关闭客户端的套接字输出流,保证服务端结束读取。
先启动FileReceiver.java
,再启动FileSender.java
,观察到文件receive.png
生成,以及FileSender.java
输出:
3 UDP网络程序示例
3.1 简单的发送器、接收器程序
发送器(MyUDPSender.java
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import java.net.*;
public class MyUDPSender { public static void main(String[] args) throws Exception { DatagramSocket socket = new DatagramSocket(); String msg = "你好!"; byte[] buffer = msg.getBytes(); InetAddress localhost = InetAddress.getByName("localhost"); int port = 9090; DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length, localhost, port); socket.send(packet); socket.close(); } }
|
接收器(MyUDPReceiver.java
)
1 2 3 4 5 6 7 8 9 10 11 12 13
| import java.net.*;
public class MyUDPReceiver { public static void main(String[] args) throws Exception { DatagramSocket socket = new DatagramSocket(9090); byte[] buffer = new byte[1024]; DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length); socket.receive(packet); System.out.println(packet.getAddress().getHostAddress()); System.out.println(new String(packet.getData(), 0, packet.getLength())); socket.close(); } }
|
说明:
- 发送器和接收器都有数据报套接字
DatagramSocket
和数据包DatagramPacket
,但使用方式不同。
- 发送器的
DatagramSocket
不用绑定端口,因为它只用send()
方法,无需绑定发送者的端口;接收器的DatagramSocket
需要绑定端口,因为它用receive()
方法需要明确自己的地址。
- 发送器的
DatagramPacket
需要绑定发送地址,因为它已有内容并打包好,需要向特定地址传递;接收器的DatagramPacket
不用绑定地址,因为它是一个空的容器,只起接收载体作用。
- 接收器第10行,
packet.getLength()
不能用packet.getData().length
代替,数据包大小与数据字节数组的大小是不同的。
先启动MyUDPReceiver.java
,观察到程序开始等待,再启动MyUDPSender.java
,观察到MyUDPReceiver.java
输出:
3.2 较复杂的聊天器程序(多线程)
聊天发送端线程(MyChatSender.java
)
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
| import java.io.*; import java.net.*;
public class MyChatSender implements Runnable { DatagramSocket socket = null; BufferedReader reader = null; private final String toIP; private final int toPort;
public MyChatSender(String toIP, int toPort) { this.toIP = toIP; this.toPort = toPort;
try { socket = new DatagramSocket(); reader = new BufferedReader(new InputStreamReader(System.in)); } catch (Exception e) { e.printStackTrace(); } }
@Override public void run() { while (true) { try { String data = reader.readLine(); byte[] dataBytes = data.getBytes(); DatagramPacket packet = new DatagramPacket(dataBytes, 0, dataBytes.length, new InetSocketAddress(toIP, toPort)); socket.send(packet); if ("bye".equals(data)) { break; } } catch (Exception e) { e.printStackTrace(); } } try { reader.close(); } catch (IOException e) { e.printStackTrace(); } socket.close(); } }
|
聊天接收端线程(MyChatReceiver.java
)
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
| import java.io.*; import java.net.*;
public class MyChatReceiver implements Runnable { DatagramSocket socket = null; private final String fromName;
public MyChatReceiver(int port, String fromName) { this.fromName = fromName; try { socket = new DatagramSocket(port); } catch (SocketException e) { e.printStackTrace(); } }
@Override public void run() { while (true) { try { byte[] container = new byte[1024]; DatagramPacket packet = new DatagramPacket(container, 0, container.length); socket.receive(packet); byte[] dataBytes = packet.getData(); String data = new String(dataBytes, 0, packet.getLength()); System.out.println(fromName + ":" + data); if ("bye".equals(data)) { break; } } catch (IOException e) { e.printStackTrace(); } } socket.close(); } }
|
聊天者A(ChatterA.java
)
1 2 3 4 5 6
| public class ChatterA { public static void main(String[] args) { new Thread(new MyChatSender("localhost", 9999)).start(); new Thread(new MyChatReceiver(8888, "B")).start(); } }
|
聊天者B(ChatterB.java
)
1 2 3 4 5 6
| public class ChatterB { public static void main(String[] args) { new Thread(new MyChatSender("localhost", 8888)).start(); new Thread(new MyChatReceiver(9999, "A")).start(); } }
|
说明:每个聊天者有两个线程,以及两个所需端口。A向localhost
的9999端口发信息,同时接收发送到8888端口名为B发来的信息;B向localhost
的8888端口发信息,同时接收发送到9999端口名为A发来的信息。
分别启动ChatterA.java
和ChatterB.java
,并输入文字,观察到两个程序分别输出:
1 2 3 4 5 6
| 你好! B:你好啊! 今天天气真不错。 B:是啊,今天天气晴朗。 bye B:bye
|
1 2 3 4 5 6
| A:你好! 你好啊! A:今天天气真不错。 是啊,今天天气晴朗。 A:bye bye
|
3.3 较复杂的广播、收音机程序
广播(MyBroadcast.java
)
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
| import java.net.*;
public class MyBroadcast extends Thread { String msg = "欢迎收听广播节目。"; int port = 9898; InetAddress ipGroup; MulticastSocket socket;
MyBroadcast() throws Exception { ipGroup = InetAddress.getByName("224.255.10.0"); socket = new MulticastSocket(port); socket.setTimeToLive(1); socket.joinGroup(ipGroup); }
@Override public void run() { while (true) { byte[] data = msg.getBytes(); DatagramPacket packet = new DatagramPacket(data, data.length, ipGroup, port); System.out.println(new String(data)); try { socket.send(packet); sleep(3000); } catch (Exception e) { e.printStackTrace(); } } }
public static void main(String[] args) throws Exception { MyBroadcast broadcast = new MyBroadcast(); broadcast.start(); } }
|
收音机(MyRadio.java
)
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 javax.swing.*; import java.awt.*; import java.awt.event.*; import java.net.*;
public class MyRadio extends JFrame implements Runnable, ActionListener { int port; InetAddress ipGroup; MulticastSocket socket; JButton start = new JButton("开始接收"); JButton stop = new JButton("停止接收"); JTextArea startArea = new JTextArea(10, 10); JTextArea receivedArea = new JTextArea(10, 10); Thread thread; boolean isStopped = false;
public MyRadio() throws Exception { super("数据报收音机"); setDefaultCloseOperation(EXIT_ON_CLOSE); thread = new Thread(this); start.addActionListener(this); stop.addActionListener(this); startArea.setForeground(Color.BLUE); JPanel north = new JPanel(); north.add(start); north.add(stop); add(north, BorderLayout.NORTH); JPanel center = new JPanel(new GridLayout(1, 2)); center.add(startArea); center.add(receivedArea); add(center, BorderLayout.CENTER); validate();
port = 9898; ipGroup = InetAddress.getByName("224.255.10.0"); socket = new MulticastSocket(port); socket.joinGroup(ipGroup);
setBounds(100, 50, 360, 380); setVisible(true); }
@Override public void run() { while (true) { byte[] data = new byte[1024]; DatagramPacket packet = new DatagramPacket(data, data.length, ipGroup, port); try { socket.receive(packet); String msg = new String(packet.getData(), 0, packet.getLength()); startArea.setText("正在接收的内容:\n" + msg); receivedArea.append(msg + "\n"); } catch (Exception e) { e.printStackTrace(); } if (isStopped) { break; } } }
@Override public void actionPerformed(ActionEvent e) { if (e.getSource() == start) { start.setBackground(Color.RED); stop.setBackground(Color.YELLOW); if (!(thread.isAlive())) { thread = new Thread(this); } thread.start(); isStopped = false; } if (e.getSource() == stop) { start.setBackground(Color.YELLOW); stop.setBackground(Color.RED); isStopped = true; } }
public static void main(String[] args) throws Exception { MyRadio radio = new MyRadio(); radio.setSize(460, 200); } }
|
说明:
- 发出广播和接收广播的地址必须位于同一个组内,地址范围是224.0.0.0~224.255.255.255,该地址不代表某个特定主机的位置。
- 加入同一个组的主机可以在某个端口广播信息,也可以在某个端口接收信息。
启动MyBroadcast.java
,观察到其开始输出;启动MyRadio.java
并点击开始接收按钮,观察到其开始接收广播信息:
1 2 3
| 欢迎收听广播节目。 欢迎收听广播节目。 欢迎收听广播节目。
|
4 利用URL下载网络资源示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import javax.net.ssl.HttpsURLConnection; import java.io.FileOutputStream; import java.io.InputStream; import java.net.URL;
public class Main { public static void main(String[] args) throws Exception { URL url = new URL("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png"); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); InputStream is = connection.getInputStream(); FileOutputStream fos = new FileOutputStream("logo.png"); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } fos.close(); is.close(); connection.disconnect(); } }
|
观察到文件logo.png
的生成。
5 I/O补充
使用输入流、输出流时以下这段代码的原理:
1 2 3 4 5 6 7
| FileInputStream fis = new FileInputStream("xxx"); OutputStream os = new OutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { os.write(buffer, 0, len); }
|
buffer
是一个字节数组,长度为1024,类似于一个缓冲区。
fis.read(buffer)
,这一步从输入流读取buffer
大小的字节(即1024),把这些字节赋给buffer
,并返回读取的字节数。
- 把读取的字节数赋值给
len
,然后对len
进行判断。
- 把
buffer
数组的0到len
位置的字节写入输出流。
- 每次循环,
buffer
就被重新赋值一次,因此文件大小与buffer
的长度1024是没有关系的。
- 读取到输入流末尾时,
read()
方法返回-1,循环结束。
- 注意:缓冲区的大小不应太小,否则会涉及到编码问题,部分字节被强行拆分到两个缓冲区时可能会出现乱码。