文章目录
- 一、网络编程入门
- 1. 网络编程三要素
- 2. IP 地址
- 3. InetAddress
- 4. 端口
- 5. 协议
- 二、UDP 通信程序
- 1. UDP 发送数据
- 2. UDP 接收数据
- 3. UDP 案例
- 三、TCP 通信程序
- 1. TCP 发送数据
- 2. TCP 接收数据
- 3. 服务器给出反馈
- 4. 客户端录入键盘数据
- 5. 服务器数据写入文件
- 6. 客户端数据来自文件
- 7. 文件上传并给出反馈
- 8. 多线程实现上传文件
一、网络编程入门
网络编程:在网络通信协议下,实现网络互连的不同计算机上运行的程序间可以进行数据交换。
1. 网络编程三要素
① IP 地址
要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而 IP 地址就是这个标识号,也就是设备的 标识。
② 端口
网络的通信,本质上是两个应用程序的通信。每台计算机都有很多的应用程序,那么在网络通信时,如何区分这些应用程序呢?如果说 IP 地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序了,也就是应用程序的标识。
③ 协议
通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换,常见的协议有 UDP 协议和 TCP 协议。
2. IP 地址
IP 地址是网络中设备的唯一标识。
IP 地址分为两大类:
IPv4:是给每个连接在网络上的主机分配一个 32bit 地址。按照 TCP/IP 规定,IP 地址用二进制来表示,每个 IP 地址长32bit,也就是 4 个字节,例如一个采用二进制形式的 IP 地址是 “11000000 10101000 00000001 01000010”,这么长的地址,处理起来也太费劲了。为了方便使用,IP 地址经常被写成十进制的形式,中间使用符号 “.” 分隔不同的字节,于是,上面的 IP 地址可以表示为 “192.168.1.66”。IP 地址的这种表示法叫做 “点分十进制表示法”,这显然比 1 和 0 容易记忆很多。
IPv6:由于互联网的蓬勃发展,IP 地址的需求量越来越多,但是网络地址资源是有限的,使得 IP 地址的分配越发紧张。为了扩大地址空间,通过 IPv6 重新定义地址空间,采用 128 位地址长度,每 16 个字节一组,分成 8 组十六进制数,这样就解决了网络地址资源数量不够的问题。
常用命令:
① ipconfig:查看本机 IP 地址;
② ping IP 地址:检查网络是否连通。
win+R 键,输入 cmd 进入命令行!
特殊 IP 地址 127.0.0.1,是回送地址,可以代表本机地址,一般用来测试使用!
3. InetAddress
为了方便我们对 IP 地址的获取和操作,Java 提供了一个类 InetAddress 供我们使用,此类表示 Internet 协议地址。
package com.zxe;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class Test {
public static void main(String[] args) throws UnknownHostException {
InetAddress address = InetAddress.getByName("LAPTOP-5061KU8Q");
String name = address.getHostName();
String ip = address.getHostAddress();
System.out.println("主机名是:" + name);
System.out.println("IP地址是:" + ip);
}
}
我们可以直接传入主机名,也可以通过传 IP 地址的方式来获取 InetAddress 对象:
InetAddress address = InetAddress.getByName("192.168.207.1");
4. 端口
端口:设备上应用程序的唯一标识。
端口号:用两个字节表示的整数,它的取值范围是 0 ~ 65535。其中,0 ~ 1023 之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用 1024 以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
5. 协议
协议:计算机网络中,连接和通信的规则被称为网络通信协议。
① UDP 协议
用户数据报协议。
UDP 是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
例如视频会议通常采用 UDP 协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大的影响。但是在使用 UDP 协议传送数据时,由于 UDP 的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用 UDP 协议。
由于使用 UDP 协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输。
② TCP 协议
传输控制协议。
TCP 协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在 TCP 连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过 “三次握手”。
三次握手:TCP 协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。第一次握手,客户端向服务器端发出连接请求,等待服务器确认;第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求;第三次握手,客户端再次向服务器端发送确认信息,确认连接。
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了,由于这种面向对象连接的特性,TCP 协议可以保证传输数据的安全,所以应用十分广泛,例如上传文件、下载文件、浏览网页等。
二、UDP 通信程序
UDP 协议是一种不可靠的网络协议,它在通信的两端各建立一个 Socket 对象,但是这两个 Socket 只是发送、接收数据的对象,因此对于基于 UDP 协议的通信双方而言,没有所谓的客户端和服务器的概念。
Java 提供了 DatagramSocket 类作为基于 UDP 协议的 Socket。
1. UDP 发送数据
发送数据的步骤:
① 创建发送端的 Socket 对象(DatagramSocket);
② 创建数据,并把数据打包;
③ 调用 DatagramSocket 对象的方法发送数据;
④ 关闭发送端。
package com.zxe;
import java.io.IOException;
import java.net.*;
public class Send {
public static void main(String[] args) throws IOException {
//创建发送端的Socket对象
DatagramSocket ds = new DatagramSocket();
//创建数据,并把数据打包,DatagramPacket(byte[] buf, int length, InetAddress address, int port)
byte[] bys = "嗨UDP,我来啦!".getBytes();
int length = bys.length;
InetAddress address = InetAddress.getByName("LAPTOP-5061KU8Q");
int port = 10072;
DatagramPacket dp = new DatagramPacket(bys, length, address, port);
//调用DatagramSocket对象的send方法发送数据
ds.send(dp);
//关闭发送端
ds.close();
}
}
2. UDP 接收数据
接收数据的步骤:
① 创建接收端的 Socket 对象(DatagramSocket);
② 创建一个数据包,用于接收数据;
③ 调用 DatagramSocket 对象的方法接收数据;
④ 解析数据包,并把数据在控制台显示,byte[] getData() 返回数据缓冲区;
⑤ 关闭接收端。
package com.zxe;
import java.io.IOException;
import java.net.*;
public class Receive {
public static void main(String[] args) throws IOException {
//创建接收端的Socket对象
DatagramSocket ds = new DatagramSocket(10072);
//创建一个数据包,用于接收数据,DatagramPacket(byte[] buf, int length)
byte[] bys = new byte[1024];
int length = bys.length;
DatagramPacket dp = new DatagramPacket(bys, length);
//调用DatagramSocket对象的receive方法接收数据
ds.receive(dp);
//解析数据包,并把数据在控制台显示,byte[] getData() 返回数据缓冲区
byte[] datas = dp.getData();
//getLength()返回实际接收到的数据的长度,因为实际数据的长度不一定是1024
int len = dp.getLength();
String dataString = new String(datas, 0, len);
System.out.println("数据是:" + dataString);
//关闭接收端
ds.close();
}
}
接收端在创建 Socket 对象时要传入指定的端口号,运行时要先执行接收端,再执行发送端,同时注意此 IP 是本机的 IP!
3. UDP 案例
要求:数据来自于键盘录入,直到输入的数据是 886,发送数据结束,因为接收端不知道发送端什么时候停止发送,故采用死循环接收。
package com.zxe;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;
public class Send {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line;
while ((line = br.readLine()) != null) {
if (line.equals("886")) {
break;
}
byte[] bys = line.getBytes();
InetAddress address = InetAddress.getByName("LAPTOP-5061KU8Q");
DatagramPacket dp = new DatagramPacket(bys, bys.length, address, 10072);
ds.send(dp);
}
ds.close();
}
}
package com.zxe;
import java.io.IOException;
import java.net.*;
public class Receive {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(10072);
while (true) {
byte[] bys = new byte[1024];
int length = bys.length;
DatagramPacket dp = new DatagramPacket(bys, length);
ds.receive(dp);
byte[] datas = dp.getData();
int len = dp.getLength();
String dataString = new String(datas, 0, len);
System.out.println("数据是:" + dataString);
}
}
}
你也可以同时运行多个发送端给一个接收端发送数据:
如何在 idea 开启多个控制台?
idea 页面上边菜单栏,Run → Edit Configurations → Modify options → 把 Allow multiple instances 勾上 → Apply。
三、TCP 通信程序
TCP 通信协议是一种可靠的网络协议,它在通信的两端各建立一个 Socket 对象,从而在通信的两端形成网络虚拟链路,两端的程序就可以通过虚拟链路进行通信。
Java 对基于 TCP 协议的网络提供了良好的封装,使用 Socket 对象来代表两端的通信端口,并通过 Socket 产生 IO 流来进行网络通信。
Java 为客户端提供了 Socket 类,为服务器端提供了 ServerSocket 类。
1. TCP 发送数据
发送数据的步骤:
① 创建客户端的 Socket 对象(Socket);
② 获取输出流,写数据;
③ 释放资源
package com.zxe;
import java.io.IOException;
import java.io.OutputStream;
import java.net.*;
public class Send {
public static void main(String[] args) throws IOException {
Socket s = new Socket("LAPTOP-5061KU8Q", 10023);
OutputStream os = s.getOutputStream();
os.write("嗨TCP,我来啦!".getBytes());
s.close();
}
}
这里直接运行是会报错的,因为我们的 TCP 是要进行三次握手的,目前没有写服务器的程序,所以会报错!
2. TCP 接收数据
接收数据的步骤:
① 创建服务器端的 Socket 对象(ServerSocket);
② 监听客户端连接,返回一个 Socket 对象;
③ 获取输入流,读数据,并把数据显示在控制台;
④ 释放资源。
package com.zxe;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
public class Receive {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10023);
Socket s = ss.accept();
InputStream is = s.getInputStream();
byte[] bys = new byte[1024];
int len = is.read(bys);
String data = new String(bys, 0, len);
System.out.println("数据是:" + data);
ss.close();
}
}
accept() 监听到客户端有连接,就生成一个 Socket 对象!
3. 服务器给出反馈
需求:客户端发送数据,接收服务器反馈,服务器接收数据,给出反馈。
package com.zxe;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.*;
public class Send {
public static void main(String[] args) throws IOException {
Socket s = new Socket("LAPTOP-5061KU8Q", 10023);
//给服务器发送数据
OutputStream os = s.getOutputStream();
os.write("嗨TCP,我来啦!".getBytes());
//接收服务器的反馈
InputStream is = s.getInputStream();
byte[] bys = new byte[1024];
int len = is.read(bys);
String data = new String(bys, 0, len);
System.out.println("客户端收到反馈:" + data);
s.close();
}
}
package com.zxe;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.*;
public class Receive {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10023);
Socket s = ss.accept();
//服务器接收到数据
InputStream is = s.getInputStream();
byte[] bys = new byte[1024];
int len = is.read(bys);
String data = new String(bys, 0, len);
System.out.println("数据是:" + data);
//服务器给出反馈
OutputStream os = s.getOutputStream();
os.write("数据成功收到!".getBytes());
ss.close();
}
}
4. 客户端录入键盘数据
需求:客户端数据来自于键盘录入,直到输入的数据是 886,发送数据结束,服务器接收的数据在控制台输出。
package com.zxe;
import java.io.*;
import java.net.*;
public class Send {
public static void main(String[] args) throws IOException {
Socket s = new Socket("LAPTOP-5061KU8Q", 10023);
//键盘录入数据
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//封装输出流对象,将字节输出流转换为字符输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line;
while ((line = br.readLine()) != null) {
if (line.equals("886")) {
break;
}
bw.write(line);
bw.newLine();
bw.flush();
}
s.close();
}
}
package com.zxe;
import java.io.*;
import java.net.*;
public class Receive {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10023);
Socket s = ss.accept();
//为了能一次读取一行,我们把字节缓冲流包装成字符缓冲流
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
System.out.println("数据是:" + line);
}
ss.close();
}
}
5. 服务器数据写入文件
需求:客户端数据来自于键盘录入,直到输入的数据是 886,发送数据结束,服务器接收到的数据写入文本文件。
package com.zxe;
import java.io.*;
import java.net.*;
public class Receive {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10023);
Socket s = ss.accept();
//为了能一次读取一行,我们把字节缓冲流包装成字符缓冲流
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter bw = new BufferedWriter(new FileWriter("idea_test\\myReceive.txt"));
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
bw.flush();
}
bw.close();
ss.close();
}
}
客户端代码与上一案例一样,这里仅展示服务器端代码!
6. 客户端数据来自文件
需求:客户端数据来自文本文件,服务器接收到的数据写入文本文件。
package com.zxe;
import java.io.*;
import java.net.*;
public class Send {
public static void main(String[] args) throws IOException {
Socket s = new Socket("LAPTOP-5061KU8Q", 10023);
BufferedReader br = new BufferedReader(new FileReader("idea_test\\mySend.txt"));
//封装输出流对象,将字节输出流转换为字符输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
bw.flush();
}
br.close();
s.close();
}
}
服务器端代码与上一案例一致,这里仅展示客户端代码!
7. 文件上传并给出反馈
需求:服务器数据来自于文本文件,接收服务器反馈,服务器将接收到的数据写入文本文件,并给出反馈。
package com.zxe;
import java.io.*;
import java.net.*;
public class Send {
public static void main(String[] args) throws IOException {
Socket s = new Socket("LAPTOP-5061KU8Q", 10023);
BufferedReader br = new BufferedReader(new FileReader("idea_test\\mySend.txt"));
//封装输出流对象,将字节输出流转换为字符输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
bw.flush();
}
s.shutdownOutput();
BufferedReader brClient = new BufferedReader(new InputStreamReader(s.getInputStream()));
String data = brClient.readLine();
System.out.println("服务器给出的反馈:" + data);
br.close();
s.close();
}
}
package com.zxe;
import java.io.*;
import java.net.*;
public class Receive {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10023);
Socket s = ss.accept();
//为了能一次读取一行,我们把字节缓冲流包装成字符缓冲流
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter bw = new BufferedWriter(new FileWriter("idea_test\\myReceive.txt"));
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
bw.flush();
}
BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
bwServer.write("文件上传成功!");
bwServer.newLine();
bwServer.flush();
bw.close();
ss.close();
}
}
shutdownOutput() 自定义结束标记,可以中断输出流,告诉服务器我已发送完毕了,但并不是关闭连接,客户端收到反馈的过程是一行一行读取的,所以服务器在给出反馈后一定要另起一行并刷新一下!
8. 多线程实现上传文件
需求:客户端数据来自于文本文件,接收服务器反馈,服务器将接收到的数据写到文本文件,并给出反馈,代码使用线程进行封装,为每一个客户端开启一个线程。
//ServerThread.java
package com.zxe;
import java.io.*;
import java.net.Socket;
public class ServerThread implements Runnable {
private Socket s;
public ServerThread(Socket s) {
this.s = s;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
int count = 0;
File file = new File("idea_test\\Copy[" + count + "].txt");
while (file.exists()) {
count++;
file = new File("idea_test\\Copy[" + count + "].txt");
}
BufferedWriter bw = new BufferedWriter(new FileWriter(file));
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
bw.flush();
}
BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
bwServer.write("文件上传成功!");
bwServer.newLine();
bwServer.flush();
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//Send.java
package com.zxe;
import java.io.*;
import java.net.*;
public class Send {
public static void main(String[] args) throws IOException {
Socket s = new Socket("LAPTOP-5061KU8Q", 10023);
BufferedReader br = new BufferedReader(new FileReader("idea_test\\mySend.txt"));
//封装输出流对象,将字节输出流转换为字符输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
bw.flush();
}
s.shutdownOutput();
BufferedReader brClient = new BufferedReader(new InputStreamReader(s.getInputStream()));
String data = brClient.readLine();
System.out.println("服务器给出的反馈:" + data);
br.close();
s.close();
}
}
//Receive.java
package com.zxe;
import java.io.*;
import java.net.*;
public class Receive {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10023);
while (true) {
Socket s = ss.accept();
ServerThread st = new ServerThread(s);
Thread th = new Thread(st);
th.start();
}
}
}
为了使每次所创建文件的文件名都不同,我们这里使用 count 变量来控制文件名称!