【计算机网络】网络编程
文章目录
- 1. 客户端/服务器
- 2. TCP/UDP协议
- 3. 网络编程套接字-socket
- 3.1 API的使用
- 3.1 DatagramScoket类
- 3.1 DatagramScoket类
- 4. 通过UDP实现回显服务器程序
- 4.1 服务器代码
- 4.2 客户端代码
- 4.3 代码执行过程
- 4.4 通过UDP实现翻译客户端
- 5. 通过TCP实现回显服务器
- 5.1 服务器代码
- 5.2 客户端代码
- 5.3 代码执行过程
1. 客户端/服务器
在网络中,主动发起请求的一方就是客户端,被动接受的一方就是服务器。
客户端发送给服务器的数据,叫做请求(request)。
服务器返回给客户端的数据,叫做响应(response)。
客户端-服务器之间的交互:
一问一答<----->场景:web开发
一问多答<----->场景:下载
多问一答<----->场景:上传
多问多答<----->场景:远程控制
2. TCP/UDP协议
进行网络通信,需要调用系统的api,本质上是传输层提供的传输层,涉及到的协议主要是TCP和UDP。
区别:
TCP | UDP |
---|---|
有连接 | 无连接 |
可靠传输 | 不可靠传输 |
面向字节流 | 面向数据报 |
全双工 | 全双工 |
连接:网络上的连接是抽象的,本质上就是通信双方保存了对方的相关信息。
有连接类似于打电话,需要对方接通
无连接类似于发短信,无需对方接通
可靠传输:这里的可靠传输就是发的数据到没到,发送方能够清楚的感知到。
面向字节流:网络中传输的数据的基本单位是字节。
面向数据报:每次传输的基本单位是数据报。
全双工:一个信道可以双向通信,就像公路一样是双向车道。
半双工:只能单向通信,就像过独木桥。
3. 网络编程套接字-socket
socket 是操作系统给应用程序(传输层给应用层)提供的API,Java对这个API进行了封装。
socket提供了两组不同的 API,UDP有一套,TCP有一套,本文主要介绍api的使用
3.1 API的使用
Java把系统原生的API进行了封装,操作系统中有一类文件叫做 scoket 文件,抽象的表示了"网卡"这样的设备,通过操作scoket文件就可以对网卡进行操作。
通过网卡发送数据,就是写scoket文件
通过网卡接收数据,就是读socket文件
3.1 DatagramScoket类
DatagramScoket是UDP scoket,用于接收和发送数据报
构造方法 | 说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(port就是端口号) |
内置方法 | 说明(下面的DatagramPacket p是作为输出型参数的) |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭数据报套接字 |
输出型参数:输出型参数是一个变量,函数会修改它的值,并将修改后的值传递回调用者。调用者可以通过这个参数获取函数处理后的数据。就像是我们自己带饭盒去食堂吃饭,饭盒就相当于DatagramPacket,打饭的阿姨会帮我们把饭盒装满饭菜,此时打饭的阿姨就是void receive。
3.1 DatagramScoket类
UDP面向数据报,每次发送接收数据的基本单位,就是一个UDP数据报。
构造方法 | 说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个 DatagramPacket 用来接收数据报,接收的数据保存在字节数组 buf 中,接收指定的长度 length |
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) | 构造一个 DatagramPacket 用来接收数据报,接收的数据保存在字节数组 buf 中,接收指定的长度 length,address 表示指定的目的主机的 ip 和端口号 |
内置方法 | 说明(下面的DatagramPacket p是作为输出型参数的) |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或者从发送的数据报中,获取接收端主机的端口号 |
byte[] getData() | 获取数据报中的数据 |
4. 通过UDP实现回显服务器程序
回显服务器(Echo Server)是一种网络服务器,其主要功能是将接收到的数据原样返回给发送者。
4.1 服务器代码
- 创建 DatagramSocket 对象,接下来操作网卡,操作网卡都是通过 socket 对象来完成的,此时创建 DatagramSocket 对象需要指定端口号,以方便客户端寻找服务器。
- 对于服务器来说,需要不断地接受请求与返回响应,所以这里一直while循环,直至被强制终止。
- receive从网卡能读取到一个 UDP 数据报,并放到了 requestPacket 对象中。
其中 UDP 数据报的载荷部分就被放到 requestPacket 内置的字节数组中。
另外报头部分,也会被 requestPacket 的其他属性保存,除了 UDP 报头之外,还有其他信息,比如收到的数据源 IP…
通过 requestPacket 获取数据报的源 ip、源端口 - 传输层会为每个socket对象分配一个缓冲区(内核里),此处给socket分配的缓冲区就是“阻塞队列”。
- 因为这个是回显服务器,所以process没有任何操作。
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
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.把响应返回给客户端
//构造一个DatagramPacket作为响应对象
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
socket.send(responsePacket);
//打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
4.2 客户端代码
- 客户端不需要手动指定端口,因为不知道用户客户端哪个端口空闲。
- 客户端给服务器发送请求时,需要知道服务器的IP地址与端口号。
- 使用 scanner 读取字符串,最好使用 next 而不是 nextLine。
如果使用 nextLine 读取,需要手动输入换行符,enter 来进行控制,由于 enter 键不仅仅会产生 \n 还会产生其他字符,就会导致读取到的内容就容易出问题。
使用 next 其实是以"空白符" 作为分隔符,包括不限于换行, 回车,空格, 制表符,垂直制表符…
package network;
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 {
this.serverIp = serverIp;
this.serverPort = serverPort;//这里是十进制位的IP地址
socket = new DatagramSocket();
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
while (true) {
//1.从控制台上读取要发送的数据
System.out.print("->");//输入
if (!scanner.hasNext()){
break;
}
String request = scanner.next();
//2.构造请求并发送
DatagramPacket requestPacker = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);//这里是改为二进制后的IP地址
socket.send(requestPacker);
//3.读取服务器的响应
DatagramPacket responsePacker = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacker);
//4.把响应显示在控制台
String response = new String(responsePacker.getData(),0,responsePacker.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
udpEchoClient.start();
}
}
4.3 代码执行过程
-
服务器启动,启动之后,立刻进入while循环,执行到receive,进入阻塞。此时没有任何客户端发来请求。
-
客户端启动,启动之后,立刻进入while循环,执行到hasNext,进入阻塞。此时用户没有在控制台输入任何内容。
-
用户在客户端的控制台中输入字符串,按下回车,此时hasNext阻塞解除,next会返回刚才输入的内容。
基于用户输入的内容,构造出一个DatagramPacket对象,并进行send。
send执行完毕之后,执行到receive操作,等待服务器返回的响应数据。 -
服务器收到请求之后,就会从receive的阻塞中返回。
返回之后,就会根据读到的DatagramPacket对象,构造String request,通过process方法构造一个String response。
再根据response构造一个DatagramPacket表示响应对象,在通过send来进行发送给客户端。
执行这个过程中,客户端始终在阻塞等待。 -
客户端从receive中进行返回,就能够得到服务器返回的响应,并且打印在控制台上。
与此同时,服务器也进入下一次循环,也要进入到第二次的receive阻塞,等待下一个请求。
4.4 通过UDP实现翻译客户端
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer{
private HashMap<String,String> hashMap = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
hashMap.put("cat","小猫");
hashMap.put("dog","小狗");
hashMap.put("chicken","小鸡");
}
//start() 方法完全从父类集成下来即可
//process() 方法要进行重写,加入咱们自己的业务逻辑,进行翻译
@Override
public String process(String request) {
return hashMap.getOrDefault(request,"您查的单词不存在");
}
public static void main(String[] args) throws IOException {
UdpDictServer udpDictServer = new UdpDictServer(9090);
udpDictServer.start();
}
}
5. 通过TCP实现回显服务器
TCP是面向字节流的,传输的基本单位是字节,TCP协议是需要建立连接的。
连接建立:从客户端Socket的构造方法发送连接请求,服务器的SerevrSocket监听到请求后并且调用accept()方法,这样就建立了连接,然后accept()方法在服务器中会生成一个新的Socket对象用来进行通信。
构造方法 | 说明 |
---|---|
ServerSocket(int port) | 创建⼀个服务端流套接字Socket,并绑定到指定端⼝ |
Socket(String host, int port) | 创建⼀个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
ServerSocket 类的内置方法:
内置方法 | 说明 |
---|---|
Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客户端连接后,返回一个Socket对象,并且基于Socket建立与客户端的连接,没有就阻塞等待 |
void close() | 关闭套接字 |
Socket 类的内置方法:
内置方法 | 说明 |
---|---|
InetAddress getInetAddress() | 返回套接字锁连接的地址 |
InputStream getInputStream() | 返回此套接字的输⼊流 |
InputStream getOutputStream() | 返回此套接字的输出流 |
ServerSocket只能在服务器中使用,而Socket既可以在服务器中使用也可以在客户端使用。
TCP是有连接的,就类似需要客户端拨打电话,服务器来接听。
5.1 服务器代码
- 通过 ServerSocket 提供的构造方法,给服务器分配一个端口号(这里我们需要注意,服务器是必须要指定端口号的,而客户端系统自己分配端口号就可以)
- 服务器的 ServerSocket 是用来监听请求的,如果有客户端发送请求,那么ServerSocket就会感知到。
- 当 ServerSocket 监测到有客户端发来连接的请求,ServerSocket 会调用accept()方法,accept()方法会返回一个新的Socket对象,这就算是建立了连接而这个新的对象就是用来与客户端通信。
- accept() 是一个可能会产生阻塞的操作,如果没有客户端连过来,会一直阻塞。
- 在客户端运行完毕之后我们要close()。
TCP的客户端只有一个,随着客户端越来越多,消耗的socket也会越来越多,如果不释放,可能会把文件描述符表占满。
serverSocket 整个程序只有唯一一个对象,并且这个对象的生命周期很长是要跟随整个程序的,这个对象无法提前关闭。只要程序退出,随着进程的销毁一起被释放即可。(不需要手动进行)
过程就像是客户端的 Socket 想要通过服务器的 ServerSocket 认识服务器中的 Socket。于是客户端的 Socket 就请求服务器的 ServerSocket 帮忙牵线搭桥,服务器的 ServerSocket 就把服务器的 Socket 的电话号码给了客户端,而客户端的构造方法就类似于给服务器拨通了电话,而当前只是在响铃,而accept()方法就类似接听,只有调用accept()的方法后才算真正建立连接。
- 陌生代码讲解
InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream();
从网卡内读数据以及往网卡内写数据,TCP中操作socket文件,对其进行读写(InputStream,OutputStream),就是在操作网卡,操作系统把网卡抽象成了一个文件。
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoSever {
private ServerSocket serverSocket = null;
public TcpEchoSever(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
//通过accept方法来“接听电话”,然后才能通信.如果没有客户端连过来,会进入阻塞状态
Socket clientSocket = serverSocket.accept();
processConnention(clientSocket);
}
}
//通过这个方法来处理一次连接,连接建立的过程中涉及到多次的请求响应交互
private void processConnention(Socket clientSocket) {
System.out.printf("[%s: %d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
//循环读取客户端的请求并返回响应
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();) {
while (true) {
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
//读取完毕,客户端断开连接,
System.out.printf("[%s: %d] 客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
//1.读取请求并解析,这里注意隐藏约定,next 读到空白符(\n 或者 空格)才结束
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应返回给客户端
//下行代码可以写会,但是这种方式不方便给返回的响应中添加 \n
// outputStream.write(response.getBytes(),0,response.getBytes().length);
//可以给 outputStream 套上一层,完成更方便的写入
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();//通过主动刷新缓冲区,确保数据真正发送出去
System.out.printf("[%s: %d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoSever tcpEchoSever = new TcpEchoSever(9090);
tcpEchoSever.start();
}
}
5.2 客户端代码
- 陌生代码讲解
Scanner scannerConsole = new Scanner(System.in);
Scanner scannerNetwork = new Scanner(inputStream);
在客户端上,scannerConsle 是在控制台中读取数据,也就是我们用户输入的时候读取数据,并且转变为 String 发送给服务器(就是通过OutputStream写入操作网卡的文件)。scannerNetwork 就是在服务器做出响应后,通过 inputStream 读取网卡上的数据 最终打印出结果。
PrintWriter writer = new PrintWriter(outputStream);
//2.把请求发送给服务器,这里使用println来发送,是为了让末尾带有\n,与服务器的scanner.next呼应
writer.println(request);
writer.flush();//通过主动刷新缓冲区,确保数据真正发送出去
PrintWriter 是 Java 中的一个类,位于 java.io 包中,用于以文本形式写入输出数据。它继承了 Writer 抽象类,提供了多种方法来方便地写入字符和字符串到文件或其他输出流中。
flush() 方法的作用是刷新缓冲区。因为IO都是比较低效的操作,一次一次读写,太麻烦。缓冲区就将先把数据放到内存缓冲区中,等攒够了数据一起发送,这样就变得高效了,而flush() 就是将缓存区刷新,将数据一点一点发送出去,不用等到满了一股脑发出去。
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket;
public TcpEchoClient(String severIp, int severPort) throws IOException {
//此处直接将ip和port传给socket对象。由于TCP是有连接的,因此socket里面会保存好这两信息,故TcpEchoClient就不用保存
socket = new Socket(severIp,severPort);
}
public void start() {
System.out.println("客户端启动");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerConsole = new Scanner(System.in);
Scanner scannerNetwork = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while(true){
//1.从控制台读取输入的字符串
System.out.print("->");
if (!scannerConsole.hasNext()){
break;
}
String request = scannerConsole.next();
//2.把请求发送给服务器,这里使用println来发送,是为了让末尾带有\n,与服务器的scanner.next呼应
writer.println(request);
writer.flush();//通过主动刷新缓冲区,确保数据真正发送出去
//3.从服务器读取响应,与服务器返回响应的逻辑相呼应
String response = scannerNetwork.next();
//4.把响应显示出来
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
5.3 代码执行过程
- 服务器启动,阻塞在accept(),等待客户端连接。
- 客户端启动,创建一个Socket对象,触发客户端与服务器之间的建立连接的操作,此时,服务器将从accpet的阻塞状态中返回。
- 从阻塞状态中返回后,进入到 processConnection 方法,这里执行到 hasNext() 产生阻塞,因为虽然建立连接,但是没有发来任何请求,hasNext() 阻塞等待请求到达。
- 客户端继续执行到 hasNext() ,等待用户向控制台写入内容。
- 用户在控制条输入内容,从 hasNext() 的阻塞返回,继续执行发送请求的逻辑,将请求发出去后,将会等待服务器的返回,此时也会由 next() 产生阻塞。
- 服务器从 hasNext() 的阻塞状态返回读取到请求内容并进行处理,构造出响应,将响应写会客户端,此时,服务器结束这次循环,开启下一轮循环,继续阻塞在 hasNext() 等待下一个请求。
- 客户端读取到响应,并显示出来,结束这次循环,继续阻塞在 hasNext() 等待用户的输入