目录
一、什么是网络编程
指⽹络上的主机,通过不同的进程,以编程的⽅式实现⽹络通信(或称为⽹络数据传输)。也就是客户端和服务器之间的通信。
++我们如果去xx视频看电视剧,也就是发出请求(我要看这个电视剧,请服务器给我响应),然后服务器就会给你这个视频的地址(响应),然后你就能看到电视剧了。++
当然,我们只要满⾜进程不同就⾏;所以即便是同⼀个主机,只要是不同进程,基于⽹络来传输数 据,也属于⽹络编程。 (由于设备有限,这里都用一个主机来示范)
++如上图:++
服务器就是个进程,客户端也是个进程。服务器为客户端提供数据。
二、网络编程的内容概念
接受端和发送端
在⼀次⽹络数据传输时:
接收端:收数据的一方,也就是网络通信的源主机。
发送端:发数据的一方,也就是网络通信的目的主机。
请求和响应
获取⼀个⽹络资源,涉及到两次⽹络数据传输,如下:
请求:请求数据的发送。
响应:响应数据的发送。
++比如:你去一个餐馆,要点餐。++
请求:你说老板我要一份酸菜鱼。响应:老板听完后做了份酸菜鱼 。
服务端和客户端
在常⻅的⽹络数据传输场景下:
服务端:提供服务的⼀⽅进程,可以提供对外服务。
客户端:获取服务的⼀⽅进程,称为客⼾端。
三、UDP和TCP协议的区别
UDP:无连接、不可靠传输、面向数据报、全双工
TCP:有连接、可靠传输、面向字节流、全双工
有无连接:这里的"连接"是一种抽象的连接,是绑定对方信息。若双方都绑定对方的信息,则是有连接,没有绑定对方的信息则是无连接。就像结婚证,一式两份上面都有自己和爱人的信息。
可不可靠传输:这个也不是我们说这个人可不可靠的那种可靠,而是这个协议它有没有"尽可能"的去传输数据。就比如要传一个信息,可能会失败,这时候采取什么样的行动呢?如果可靠,它可能会进行重新发送啊,或者过一会儿发送什么什么的;但是不可靠传输只管传,其他什么都不会做,所以是不可靠传输。
面向字节流,面向数据报:这里指的是传输的基本单位是什么,面向字节流的基本是字节流,而面向数据报的基本单位是数据报。两者不同,传输的方式也会不一样。
全双工:同个时间内,A->B和B->A可以同步进行。而半双工则是同个时间,只能单向进行。
四、UDP网络编程的类和函数(回显服务器)
DatagramSocket
DatagramSocket 是UDP Socket,⽤于发送和接收UDP数据报。
DatagramSocket 构造⽅法:
|--------------------------|----------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建⼀个UDP数据报套接字的Socket,绑定到本机任 意⼀个随机端⼝(⼀般⽤于客⼾端) |
| DatagramSocket(int port) | 创建⼀个UDP数据报套接字的Socket,绑定到本机指 定的端⼝(⼀般⽤于服务端) |
DatagramSocket ⽅法:
|--------------------------------|---------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该⽅法会阻塞等待) |
| void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
| void close() | 关闭此数据报套接字 |
DatagramPacket
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket 构造⽅法:
|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramPacket(byte[] buf, int length) | 构造⼀个DatagramPacket以⽤来接收数据报,接收的 数据保存在字节数组(第⼀个参数buf)中,接收指定 ⻓度(第⼆个参数length) |
| DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造⼀个DatagramPacket以⽤来发送数据报,发送的 数据为字节数组(第⼀个参数buf)中,从0到指定⻓ 度(第⼆个参数length)。address指定⽬的主机的IP 和端⼝号 |
DatagramPacket ⽅法:
|--------------------------|---------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发 送的数据报中,获取接收端主机IP地址 |
| int getPort() | 从接收的数据报中,获取发送端主机的端⼝号;或从 发送的数据报中,获取接收端主机端⼝号 |
| byte[] getData() | 获取数据报中的数据 |
构造UDP发送的数据报时,需要传⼊ SocketAddress ,该对象可以使⽤ InetSocketAddress 来创建。
InetSocketAddress
InetSocketAddress ( SocketAddress 的⼦类 )构造⽅法:
|-----------------------------------------------|-------------------------|
| 方法签名 | 方法说明 |
| InetSocketAddress(InetAddress addr, int port) | 创建⼀个Socket地址,包含IP地址和端⼝号 |
基于UDP的回显服务器和客户端:
UDP回显服务器
java
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);
// 上述收到的数据, 是二进制 byte[] 的形式体现的. 后续代码如果要进行打印之类的处理操作
// 需要转成字符串才好处理.
String request=new String(requestPacket.getData(),
0,requestPacket.getLength());
// 2. 根据请求计算响应, 由于此处是回显服务器. 响应就是请求.
String respond=process(request);
// 3. 把响应写回到客户端
DatagramPacket respondPacket =new DatagramPacket(respond.getBytes(),
0,respond.getBytes().length,requestPacket.getSocketAddress());
socket.send(respondPacket);
// 4. 把日志打印一下.
System.out.printf("[%s:%d] req=%s,res=%s\n",requestPacket.getAddress(),
requestPacket.getPort(),request,respond);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer=new UdpEchoServer(9090);
udpEchoServer.start();
}
}
1)用DatagramSocket创建出一个UDP服务器,参数为端口号(比如9090,就是约定客户端找到服务器的地方,就像约会地点);这里不需要ip地址,因为每个机子才有ip地址,所以直接在客户端new一个对象,参数输入服务器的ip地址就行了。
2)开始服务器,由于服务器基本上都是7*24小时运行的,所以我们用个while循环来一直给它运行;
3)我们用DatagramPacket来接受服务器传来的数据报,传满这个byte数组,然后包装成DatagramPacket对象,让服务器来接收;
4)我们现在是学习,为了方便查看里面的数据,所以我们把数据报转成字符串,给服务器去响应。
5)写一个process响应函数(回显服务器,其实就是return客户端发来的信息),来响应客户端发来的信息,这里我们就把刚刚的字符串传进去,然后返回同样的数据。
6)把这个返回来的信息,我们拿DatagramPacket来包装起来,发回给客户端,这样就完成了响应。(注意的是,发回去的时候要传客户端的ip和端口号,不然都不知道要传给谁)
7)打印日志。
UDP回显客户端
java
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;
}
private void start() throws IOException {
System.out.println("启动客户端");
// 1. 从控制台读取到用户的输入.
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("->");
String request = scanner.next();
// 2. 构造出一个 UDP 请求, 发送给服务器.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes()
, request.getBytes().length, InetAddress.getByName(serverIP), this.serverPort);
socket.send(requestPacket);
// 3. 从服务器读取到响应
DatagramPacket respondPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(respondPacket);
String respond = new String(respondPacket.getData(), 0, respondPacket.getLength());
// 4. 把响应打印到控制台上.
System.out.println(respond);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
udpEchoClient.start();
}
}
1)我们UDP的客户端的构造函数是要有服务器IP和端口号的,因为客户端要主动发数据报给服务器,但是new客户端对象的时候却不需要,因为服务器最开始不需要主动找客户端,而且指定端口号的话,可能会和客户的电脑中的端口号冲突,就会产生bug;
2)因为客户端可能7*24小时都有人发数据,所以我们也是做一个while循环,输入字符串,这时候我们构建DatagramPacket来接受一下,所以这个字符串传进来的时候要getByte()一下,然后send发送这个datagrampacket对象;
3)然后等待响应,接收服务器传来的响应,也是用DatagramPacket来接收;
4)为了方便观看,也是转成字符串,打印出来。
五、TCP网络编程的类和函数(回显服务器)
ServerSocket
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造⽅法:
|------------------------|----------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建⼀个服务端流套接字Socket,并绑定到指定端⼝ |
ServerSocket ⽅法:
|-----------------|------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端Socket对象,并基于该 Socket建⽴与客⼾端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
因为TCP是有连接的,所以需要accept来连接一下,但这里的连接是抽象的连接(这里的连接我们是察觉不到的,是系统内核做的),这里的accept类似于做了一个"确认连接"的作用。
就像打电话,A打给B,会经历一系列的数据传输(比如给基站发数据说我要大给B,但这里我们是察觉不到的,很快B就响铃了)这时B接通了,就相当于accept连接完成了。
Socket就像去揽客,,然后accept连接出来的是一个客户端对象(网卡),把它揽进来,相当于我们对它进行一对一的服务。
Socket
Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服 务端Socket。
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据的。
Socket 构造⽅法:
|-------------------------------|-------------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host, int port) | 创建⼀个客⼾端流套接字Socket,并与对应IP的主机 上,对应端⼝的进程建⽴连接 |
Socket ⽅法:
|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输⼊流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
基于TCP的回显服务器和客户端:
TCP回显服务器
java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器!");
ExecutorService service= Executors.newCachedThreadPool();
while (true){
Socket clientSocket=serverSocket.accept();
//不止可以创建线程,还可以使用线程池
// Thread t=new Thread(()->{
// processConnection(clientSocket);
// });
// t.start();
//可以使用线程池,效率更高
service.submit(()->{
processConnection(clientSocket);
});
}
}
// 针对一个连接, 提供处理逻辑
private void processConnection(Socket clientSocket) {
// 先打印一下客户端的信息
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
// 获取到 socket 中持有的流对象.
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()) {
// 使用 Scanner 包装一下 inputStream. 就可以更方便的读取这里的请求数据了.
Scanner scanner=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
while (true){
// 1. 读取请求并解析
if (!scanner.hasNext()){
// 如果 scanner 无法读取出数据, 说明客户端关闭了连接, 导致服务器这边读取到 "末尾"
break;
}
String request=scanner.next();
// 2. 根据请求计算响应
String respond=process(request);
//3. 把响应写回给客户端
// 此处可以按照字节数组直接来写, 也可以有另外一种写法.
// outputStream.write(response.getBytes());
writer.println(respond);
writer.flush();
// 4. 打印日志
System.out.printf("[%s:%d],req=%s res=%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,respond);
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server=new TcpEchoServer(9090);
server.start();
}
}
1)用ServerSocket创建出一个TCP服务器,参数为端口号(比如9090,就是约定客户端找到服务器的地方,就像约会地点),这里不需要ip地址,因为每个机子才有ip地址,所以直接在客户端new一个对象,参数输入服务器的ip地址就行了。
2)因为服务器都是7*24小时运行的,所以我们用while循环把他们框起来,然后因为客户很多,频繁的创建和开销很消耗资源,所以我们考虑使用线程池来解决这个问题,注意这里使用的是newCachedThreadPool(),因为这个方法的线程池数量很大,21亿可以解决这个问题。
3)我们通过服务器的accept()方法来接收客户端对象,用Socket接收,得到的就是操作客户端的网卡;
4)因为TCP是面向字节流的,所以我们使用IO操作,来获取这些字节流,为了方便,我们把inputstream对象读到的字节传到scanner里面,这样非常的方便读取。同理outputstream作为参数传入printwriter里面,为什么用这个呢,因为这个输出的时候是需要结尾为空白符作为结束标志的,而printwriter有个方法println,会有\n作为结束标志,就不要手动打\n了。这样数据流就取一个完整的数据流了,就不会和下一个数据流混一起了。
5)其余的看看源代码,其实也差不多和UDP类似,就那么一些出入而已。
6)flush()是"冲刷缓存区",IO操作时间长,很消耗资源,所以内核优化了一下,得攒一堆IO才会一起发送,不然的话就会卡着,发不出去。
7)打印日志
Tcp回显客户端
java
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket clientSocket=null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
clientSocket=new Socket(serverIP,serverPort);
}
public void start(){
System.out.println("客户端启动!");
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()) {
Scanner scanner=new Scanner(inputStream);
Scanner scannerIn=new Scanner(System.in);
PrintWriter writer=new PrintWriter(outputStream);
while (true){
// 1. 从控制台读取到用户的输入.
System.out.print("->");
String request=scannerIn.next();
// 2. 构造出一个 UDP 请求, 发送给服务器.
writer.println(request);
writer.flush();
// 3. 从服务器读取响应
if(!scanner.hasNext()){
break;
}
String respond=scanner.next();
// 4. 打印响应结果
System.out.println(respond);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
1)我们UDP的客户端的构造函数是要有服务器IP和端口号的,因为客户端要主动发数据报给服务器,但是new客户端对象的时候却不需要,因为服务器最开始不需要找客户端,而且指定端口号的话,可能会和客户的电脑中的端口号冲突,就会产生bug。
2)接下来的基本上与前面几个同理,看看代码,这里就不赘述了。