上一章聊到了一些网络通信基础的概念,也举了例子,体会了以下大概的过程(微信聊天)。
什么是网络编程?
网络通常指的是计算机中的互联网,是由多台计算机通过网线或其他媒介相互连接组成的,编写基于网络的应用程序的过程称之为网络编程。
网络上的主机通过不同进程,以搬移的形式实现的网络通信(完成的数据传输);
当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。
网络编程又叫套接字,实现网络通信的两端就是套接字。分为服务器对应的套接字和客户端对应的套接字。
网络传输中的基本概念
发送端和接收端:
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
请求和响应:
一般来说,获取一个网络资源,涉及到两次网络数据传输:
- 第一次:请求(request)数据的发送
- 第二次:响应(response)数据的发送
客户端和服务端:
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
对于服务来说,一般是提供:
- 客户端获取服务资源
- 客户端保存资源在服务端
常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
- 客户端先发送请求到服务端
- 服务端根据请求数据,执行相应的业务处理
- 服务端返回响应:发送业务处理结果
- 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)
Socket(套接字)
我们程序员编写网络编程主要是在应用层,其他层全部被打包好了。
真正要发送某个数据,需要上层调用下层协议,下层协议为上层提供一组 api 。
这个 api 就是Socket 。
这个Socket api本身是由 C语言实现的,Java又是基于C 语言和C++ 实现的,那么Java就将其打包成具有Java风格的 api。
我们一般将 Socket 分为三类:流套接字、数据报套接字、原始套接字(这里不涉及,因为用的很少)。
我们这一章,主要涉及到前面两种套接字的编写。
数据报套接字:使用传输层UDP协议
流套接字:使用传输层TCP协议
基于 UDP 的api
我们先来看看系统提供的 api 有哪些方法:
DatagramSocket API
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报
DatagramSocket 构造方法
方法签名 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用 于服务端) |
服务器这里的socket 对象,必须关联一个 端口;
而服务器,则不需要手动关联,系统会自动分配一个空闲的端口。
我们举个例子:服务器就好像一个食堂某个窗口,而客户端就好像来用餐的人。
窗口需要提供 IP 地址(具体是哪个食堂),和 端口号(具体是哪个窗口),而用餐的人只需要 找到这个 IP地址和端口号,就能让窗口为我们提供服务。
再来看看 DatagramSocket 为用户提供的方法:
方法签名 | 方法说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻 塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
我们网络编程本质上也是个文件,既然是文件,就需要使用完后即关闭。
DatagramPacket API
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket 构造方法:
方法签名 | 方法说明 |
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节 address指定目的主机的IP和端口 |
上面的一个不需要设置地址进去,通常用来接收消息;
下面的一个需要设置地址进去,通常用来发送消息。
DatagramPacket 方法:
方法签名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取 接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获 取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
等会,写代码的时候,边看代码边使用。
基于UDP 的服务器
基于上述所说,我们来编写一个最基础的服务器,回显服务器(Echo Service)
回显服务器:客户端发送什么,服务器就返回什么。
先来编写服务器:
编写服务器的三大步骤:
- 读取请求并解析
- 根据请求计算相应
- 将响应返回到客户端
回响客户端和服务器
服务器:
我们先来看看服务器的代码:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class udpEchoService {
// 创建一个 socket 对象
// 想要实现一个网络编程必须要 有一个 socket 对象
private DatagramSocket socket = null;
// 绑定一个窗口,不一定能成功
// 如果这个端口被其他进程占用,那么这么链接就会出错
// 同一时刻,一个主机,一个进程,只能绑定一个 端口
public udpEchoService(int port) throws SocketException {
this.socket = new DatagramSocket(port);
}
// 启动
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
// 每次循环需要做三件事
// 1.读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
// 为了方便处理,这里把这个数据包转成 String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
// 2. 根据请求计算响应(此处省略这个步骤)
String response = process(request);
// 3.把相应结果写到客户端
// 根据request 字符串构造一个DatagramPacket
// 和请求 packet 不同, 此处构造响应的时候, 需要指定这个包要发给谁.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.length(),
// requestPacket 是从客户端收来的, getSocketAddress() 会得到客户端的 IP 和 端口
requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
// 这个方法希望是根据请求计算响应.
// 由于咱们写的是个 回显 程序. 请求是啥, 响应就是啥!!
// 如果后续写个别的服务器, 不再回显了, 而是有具体的业务了, 就可以修改 process 方法,
// 根据需要来重新构造响应.
// 之所以单独列成一个方法, 就是想让同学们知道, 这是一个服务器中的关键环节!!!
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
udpEchoService udpEchoServer = new udpEchoService(1010);
udpEchoServer.start();
}
}
我们简单的来介绍一下服务器:
process 方法啥也不干,以后写其他服务器的时候,根据需求再写代码。
这样一个简单的回显服务器就写好了。
客户端:
我们再来看看回显客户端:
代码:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class udpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
// 客户端启动, 需要知道服务器在哪里!!
public udpEchoClient(String serverIP, int serverPort) throws SocketException {
// 对于客户端来说, 不需要显示关联端口.
// 不代表没有端口, 而是系统自动分配了个空闲的端口.
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动");
// 通过这个客户端可以多次和服务器进行交互.
Scanner scanner = new Scanner(System.in);
while (true) {
// 分四步走:
// 1. 先从控制台, 读取一个字符串过来
// 先打印一个提示符, 提示用户要输入内容
System.out.print("-> ");
String request = scanner.next();
// 2. 把字符串构造成 UDP packet, 并进行发送.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
// 3. 客户端尝试读取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 4. 把响应数据转换成 String 显示出来.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
udpEchoClient client = new udpEchoClient("127.0.0.1", 1010);
client.start();
}
}
同样我也来稍稍解析一下代码:
我们这里比服务器多了两个属性: IP 的 端口号。
我们的客户端需要去找到服务器的位置,而服务器不需要去找客户端的位置 。
这样一个简单的回显客户端和服务器就写好了。
我们来看看效果:先启动服务器再启动客户端,否则服务器无法接收到客户端发来的请求。
分析代码执行过程
我们再来分析一下代码执行过程:
- 服务器先执行,走到receive ,线程阻塞
- 客户端启动,从控制台输入信息,并发送(send)给服务器。 第三步开始分两步走,一个是客户端,一个是服务器
- 客户端这边,继续往后走,走到receive 读取响应,造成阻塞等待
- 服务器这边,就从receive返回,读到请求数据(客户端发送的),往后走到process 拿到响应,再发送响应
- 客户端收到服务器send 回来的数据后,阻塞解除,继续执行后面的操作。
- 服务器进入下一次循环,再次阻塞在 receive ,等待客户端的请求
- 客户端继续下一轮循环,阻塞再 scanner.next 等待用户输入
这样基于udp 的客户端服务器就做好了,下一章来看看基于tcp的客户端服务器。