一、概要
学习网络编程需要向对网络模型有一定的了解,主要需要了解的网络模型有OSI
参考模型和TCP/IP
参考模型,现在TCP/IP
模型应用最为广泛,网络编程一般都是针对TCP/IP
协议参考模型的编程。但是作为学习时,OSI
的学习也是必不可少的,OSI
分为七层协议,分别是:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。TCP/IP
模型只有四层,分别是:网络访问层、互联网层、传输层和应用层。接下来要学习的内容主要在传输层(UDP
和TCP
协议)。
二、TCP
、UDP
和套接字
说道TCP
和UDP
不得不对两者进行一次比较,首先TCP
和UDP
的相同之处在于其二者都是传输层的协议,提供端到端服务。不同之处在于:
UDP
:UDP
不需要建立连接,协议头部简单,不需要反馈帧,适用于对数据准确度要求不到量大,允许丢帧出现,每个暑假的大小限制在64K内,是一种不可靠连接,但是速度快。适用于对数据可靠度要求不高,占用带宽较大,允许丢帧,传输延迟小的情况:如在线播放视频、实时语音/视频聊天、远程监控等。TCP
:TCP
传输数据前需要先建立连接,即三次握手,断开连接时需要四次以释放连接,协议头部数据复杂,数据需要反馈帧来确保数据的每一帧都成功送达,是一种可靠连接,但是效率较UDP
稍低。TCP
适合用于对数据可靠度要求高的情况:如文件传输、浏览网页、网络聊天、控制信息等。套接字是
TCP/IP
的基本单元,一个IP地址和一个端口号在一起可以称为一个套接字,针对不同的协议UDP
对应的套接字为DatagramSocket
,TCP
对应的套接字为Socket
和ServerSocket
,是为网络服务提供的一种机制,通信的两端都能拿到Socket
,网络通信其实就是Socket
之间的通信,数据在两端通过Socket
的IO
进行传输。通信如下图所示:
套接字间的通信 |
三、UDP
Java中操作UDP
协议的相关类为DatagramSocket
是用来发送和接收数据报包的套接字,数据报套接字是包投递服务的发送或接收点。每个在数据报套接字上发送或接收的包都是单独编址和路由的。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。 、DatagramPacket
是UDP
的数据包,用于UDP
数据的封装。
一般使用UDP
进行数据传输时的步骤如下:
- 建立
UDP
套接字服务。 - 封装待发送的数据(用
DatagramPacket
类进行封装)。 - 使用第一步建立的套接字服务将数据发送出去(使用
send(DatagramPacket)
方法)。 - 关闭资源。
UDP数据的一般接收步骤如下:
- 建立
UDP
套接字服务,指定监听的端口。 - 定义数据包,用于存储待接收的数据。
- 通过
UDP
套接字服务接收数据(使用receive(DatagramPacket)
方法接收数据)。 - 通过数据包取出接收到的数据。
- 关闭资源。
其中套接字的receive
方法是一种线程阻塞式的方法,如同读取键盘输入数据一样,没有数据会等待数据,当数据到来时会唤醒线程。下面通过一个简易聊天程序演示UDP
套接字服务的使用:
import java.io.*;
import java.net.*;
// 定义发送线程
class ChatSender implements Runnable{
private DatagramSocket mSocket;
public ChatSender(DatagramSocket socket) {
mSocket = socket;
}
public void run() {
try {
// 用于读取键盘输入数据
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in));
String line = null;
while((line = reader.readLine()) != null) {
// 当输入end后就不再发送
if(line.equals("end")) break;
// 封装发送数据包
DatagramPacket packet =
new DatagramPacket(line.getBytes(),line.length(),
InetAddress.getByName("127.0.0.1"), 8888);
// 发送数据
mSocket.send(packet);
// 接收回复数据
byte[] buf = new byte[1024];
DatagramPacket rpacket = new DatagramPacket(buf, buf.length);
mSocket.receive(rpacket);
int len = rpacket.getLength();
String data = new String(rpacket.getData(), 0, len);
// 显示回复数据
System.out.println("回复:"+data);
}
// 关闭流和套接字
reader.close();
mSocket.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
class ChatReceiver implements Runnable {
private DatagramSocket mSocket;
public ChatReceiver(DatagramSocket socket) {
mSocket = socket;
}
public void run() {
try {
while(true) {
// 接收消息数据
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
mSocket.receive(packet);
int len = packet.getLength();
// 回复数据(转成大写)
String data = new String(packet.getData(), 0, len).toUpperCase();
if(data.contains("END")) break;
// 封装回复数据
DatagramPacket spacket =
new DatagramPacket(data.getBytes(),data.length(),
InetAddress.getByName("127.0.0.1"), packet.getPort());
mSocket.send(spacket);
}
mSocket.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
class ChatDemo {
public static void main(String[] args) throws Exception{
// 创建一个udp套接字给发送者
new Thread(new ChatSender(new DatagramSocket())).start();
// 创建一个监听8888端口的套接字给接收者
new Thread(new ChatReceiver(new DatagramSocket(8888))).start();
}
}
程序的运行效果如下图所示,每当输入一条数据,然后发送,便会得到一条回应,内容是将发送过去的内容装换成大写并返回。
UDP 聊天程序运行效果 |
另外值得说明的是,UDP
可以发送广播数据,当将目标地址写为192.168.0.255
这种以255结尾的类型IP地址时,此地址为广播地址,在同一个局域网,并且在同一个网段时(也就是地址为192.168.0.1-192.168.0.254
内时,便都可以接收到其发送的数据),也可以将目标地址设置为255.255.255.255
,这种广播发送的范围会更加广泛,一般整个局域网都可以接收到(如果路由器不丢弃的话)。这种功能可用于扫描等,如飞秋上线时通知其他同局域网内的用户。
四、TCP
TCP
用于可靠数据的传输,当数据不能丢包或者不能出错时都会使用TCP
协议来传输数据,既然可靠性提高了,那么必须牺牲点什么,TCP
牺牲的便是效率,TCP
连接通过三次握手建立了有连接的服务,其三次握手可以简单地说如三句话,“我来了”,“欢迎欢迎”,“谢谢”。也就是第一次高速对方我需要请求连接,第二次对方回应你的请求,第三此我收到了对方的请求然后回应对方正式开始。
与UDP
操作上的不同再用,TCP
将请求者和被请求者分别称为客户端和服务端,对应的套接字类分别为Socket
和ServerSocket
,TCP
的数据不用封装成数据包,二是通过IO
流相互通信。
下载文件,上传文件等一般都是使用TCP
协议,下面通过一个示例演示通过TCP
协议进行文件的并发上传操作,文件的并发上传需要注意的是要为每个Socket
单独启动一个线程处理上传任务,不能让任何动作影响ServerSocket
的accept()
方法,这样才能保证并发上传文件。示例代码如下:
import java.io.*;
import java.net.*;
class UploadClient {
public static void main(String[] args) throws Exception {
if(args.length < 1) {
System.out.println("请指定一个文件"); return ;
}
File file = new File(args[0]);
if(!(file.exists() && file.isFile())) {
System.out.println("文件不存在"); return ;
}
// 上传到指定的位置
Socket socket = new Socket("127.0.0.1", 8888);
BufferedInputStream bin =
new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream bout =
new BufferedOutputStream(socket.getOutputStream());
byte[] buf = new byte[1024];
int len = 0;
// 将文件写入流中
while((len=bin.read(buf)) != -1) {
bout.write(buf, 0, len);
}
//告诉服务端数据已写完
socket.shutdownOutput();
// 读取服务端的回复
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
int num = in.read(buffer);
System.out.println(new String(buffer, 0, num));
bin.close();
socket.close();
}
}
class UploadServer {
public static void main(String[] args) throws Exception {
ServerSocket mServerSocket = new ServerSocket(8888);
while(true) {
// 接收客户端的请求
Socket socket = mServerSocket.accept();
// 但接受到了客户端的上传后,便为其开启一个线程用于上传数据
new Thread(new FileUploadThread(socket)).start();
}
//mServerSocket.close();
}
}
class FileUploadThread implements Runnable {
private Socket mSocket;
FileUploadThread(Socket socket) {
mSocket = socket;
}
public void run() {
// 获取客户端的ip
String ip = mSocket.getInetAddress().getHostAddress();
try {
System.out.println(ip + "开始上传");
BufferedInputStream bin =
new BufferedInputStream(mSocket.getInputStream());
// 用客户端ip和当前时间作为文件名称
String filename = "files\\" + ip + "-"
+ Long.toHexString(System.currentTimeMillis()) + ".jpg";
File file = new File(filename);
BufferedOutputStream bout =
new BufferedOutputStream(new FileOutputStream(file));
byte[] buf = new byte[1024];
int len = 0;
while((len=bin.read(buf))!=-1) {
bout.write(buf, 0, len);
}
// 上传结束后,回复客户端一句“上传成功”
OutputStream out = mSocket.getOutputStream();
out.write("上传成功".getBytes());
bout.close();
mSocket.close();
}
catch (Exception e) {
throw new RuntimeException(ip+"上传失败");
}
}
}
使用ServerSocket
完成一个简易的服务器,然后使用浏览器访问这个服务器,根据不同的访问内容,返回不同的响应内容,如访问"/Hello"
返回“你好啊!”,访问"/Time"
返回当前时间等,示例代码如下:
import java.io.*;
import java.net.*;
import java.util.*;
import java.text.*;
class HttpServer {
public static void main(String[] args) throws Exception {
ServerSocket mServer = new ServerSocket(8888);
while(true) {
// 接受客户端请求
Socket socket = mServer.accept();
// 获取请求流
BufferedInputStream bin =
new BufferedInputStream(socket.getInputStream());
byte[] buffer = new byte[1024];
int len = bin.read(buffer);
// 获取请求内容如"/index.html"为"127.0.0.1/index.html"的
String request = new String(buffer, 0, len).split(" ")[1];
System.out.println(request);
// 回复流
BufferedOutputStream bout =
new BufferedOutputStream(socket.getOutputStream());
// 请求"http://127.0.0.1/Hello"时返回"你好啊!"
if(request.equalsIgnoreCase("/Hello")) {
bout.write("你好啊!".getBytes());
} else if(request.equalsIgnoreCase("/Time")) {
// "/Time"时,返回当前时间
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy:MM:DD HH:mm:ss");
String time = dateFormat.format(new Date());
bout.write(time.getBytes());
} else if(request.equalsIgnoreCase("/Exit")) {
// "/Exit"时,关闭服务器
bout.write("已暂停服务".getBytes());
break;
}
bout.close();
}
mServer.close();
}
}
在浏览器中访问"http://localhost:8888/time"
时,会获取服务器的当前时间,如下图:
浏览器访问效果图 |
五、其他
URL
类:类 URL 代表一个统一资源定位符,它是指向互联网“资源”的指针。资源可以是简单的文件或目录,也可以是对更为复杂的对象的引用,例如对数据库或搜索引擎的查询。URLConnection
类:代表程序和URL
之间的通信链接,一般的使用- 通过在 URL 上调用 openConnection 方法创建连接对象。
- 处理设置参数和一般请求属性。
- 使用 connect 方法建立到远程对象的实际连接。
- 远程对象变为可用。远程对象的头字段和内容变为可访问。
通过
URLConnection
从网络上下载一张图片,将其保存至本地。示例代码如下:
import java.net.*;
import java.io.*;
class URLConnectionDemo {
public static void main(String[] args) throws Exception {
// 一个网络图片地址
URL url = new URL("http://www.itheima.com/images_new/logo.jpg");
// 打开网络连接
URLConnection conn = url.openConnection();
// 输入流
BufferedInputStream bin = new BufferedInputStream(conn.getInputStream());
// 输出流,用于保存下载的图片文件
BufferedOutputStream bout =
new BufferedOutputStream(new FileOutputStream("pic.jpg"));
byte[] buf = new byte[1024];
int len = 0;
while((len = bin.read(buf)) != -1) {
bout.write(buf, 0, len);
}
// 关闭流
bin.close();
bout.close();
}
}
InetSocketAddress
类:此类实现 IP 套接字地址(IP 地址 + 端口号)。即这个类将IP地址和端口封装在了一起,使用起来更加方便些。- 域名解析:是指解析出域名指向网站的
IP
地址,一般域名解析的步骤为:首先查看系统hosts文件中是否已经存在对应的域名关系,若已经存在,则之间访问其对应的ip,如localhost
对应的ip为127.0.0.1
,这是本机的回环地址,访问127.0.0.1
就是在访问本机自身。如果hosts文件中没有待解析的域名,则系统会启动域名解析服务,即想域名解析服务器请求对应的ip地址(这些域名解析服务器一般都是固定的公共域名解析服务器,电信的、联通的等),找到域名对应的地址后便可以请求改地址。