一.网络编程基础
什么是网络编程?
网络编程,指网络上的主机,通过不同的进程,以编程的方式,实现网络通信(或称为网络数据传输)
网络编程中的基本概念:
发送端:数据的发送方进程,称为发送端,发送端主机即网络通信中的源主机
接收端:数据的接收方进程,称为接收端.接收端主机即网络通信中的目的主机
收发端:发送端和接收端两端,简称收发端
注意:发送端和接收端是相对的,只是一次网络数据传输产生数据流向后的概念.
请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输
第一次,请求数据的发送;第二次,响应数据的发送
客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以对外提供服务
客户端:获取服务的一方进程,叫做客户端

常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
1.客户端先发送请求到服务端
2.服务端根据请求数据,执行相应的业务处理
3.服务端返回响应:发送业务处理结果
4.客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)
二.Socket
学习网络编程,我们最主要的就是要了解使用传输层的API
我们写的程序都处于应用层,从传输层->物理层这个过程都由操作系统/硬件/驱动实现好了的.
我们只需要做的就是调用系统API,将数据从应用层传到传输层
传输层提供的系统API,也称为socket api(网络编程套接字)
传输层涉及到的协议有UDP协议和TCP协议,这两个协议提供了两组socket api
UDP协议:无连接,不可靠传输,面向数据报,全双工
TCP协议:有连接,可靠传输,面向字节流,全双工
什么是有连接?就是彼此双方保存了对方的关键信息,无连接则是没有保存
什么是可靠传输?就是会对抗丢包这个情况,TCP会尽可能保证数据报能够被对方收到,UDP在发出数据报之后就不管了
什么是面向数据报,面向字节流?就是UDP读写数据的基本单位是数据报,TCP读写数据的基本单位是字节(类似于文件操作)
什么是全双工?就是一个信道,可以双向通信
关于socket api,系统提供了几个类,DategrameSocket,DategramePacket,ServerSocket,Socket
1.DategrameSocket
DategrameSocket代表了操作系统中的socket文件
所谓文件,是硬盘硬件设备的抽象,读写文件的时候,本质上是在操作硬盘
Java中创建一个DategrameSocket对象,就是在操作系统中,打开了一个socket文件.
这样的socket文件,就代表了网卡,使用socket文件也是需要打开,读写,然后关闭的,每打开一个socket文件,也是需要消耗一个文件描述符表.
通过这个对象,写入数据,就是在通过网卡发送数据,通过这个对象读取数据,就是在通过网卡接收数据

2.DategramePacket
DategramePacket是UDP socket发送和接受的数据报,是数据传输的基本单位


3.ServerSocket
ServerSocket是创建TCP服务器Socket的API

4.Socket
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket
不管是客户端还是服务端Socket,都是双方建立连接以后,保存对端信息,以及用来与对方收发数据的

三.UDP
关于socket类,UDP协议使用的是DategrameSocket和DategramePacket这两个类
由于刚学习网络编程这块,所以我们简单学习一个回显服务器
什么是回显服务器?就是客户端发送啥内容,服务器就返回啥内容
其实这个代码很简单,我先把总代码放出来,然后一步一步给大家拆分讲解
服务器代码:
java
package network.UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class EcoServer {
// 服务器端,这是一个回显服务器
// 先创建一个Socket对象
private DatagramSocket socket = null;
// 构造方法,通过port指定端口号
public EcoServer(int port) throws Exception{
socket = new DatagramSocket(port);
}
//服务器启动方法,详细业务逻辑
public void run() throws IOException{
System.out.println("服务器启动成功,端口号为:"+socket.getLocalPort());
// 服务器端需要一直运行,所以使用while循环
while(true){
// 1. 读取请求并解析
//(1)构造一个空白的DategramePacket对象
DatagramPacket receivePacket = new DatagramPacket(new byte[1024],1024);
//(2)通过receive方法接收客户端发送的数据包,没有接收到就会产生阻塞效果
socket.receive(receivePacket);
//(3)把DategramePacket数据解析成字符串
String request = new String(receivePacket.getData(),0,receivePacket.getLength());
//2.计算响应
String response = process(request);
//3.把响应写回到客户端
//(1)把响应先构造成一个DategramePacket对象
DatagramPacket responsePacket = new DatagramPacket(
response.getBytes(),
response.getBytes().length,
receivePacket.getSocketAddress()
);
//(2)通过send方法把响应发送给客户端
socket.send(responsePacket);
//4.打印日志
System.out.printf("[%s:%d],req=%s,resp=%s\n",
receivePacket.getSocketAddress(),
receivePacket.getPort(),
request,
response
);
}
}
//由于是回显服务器,所以发送什么,就返回什么
public String process(String request) {
return request;
}
public static void main(String[] args) throws Exception {
//这里的9090是指的是服务器的端口号,每一个socket对象都会占用系统的一个端口号
EcoServer server = new EcoServer(9090);
server.run();
}
}
客户端代码:
java
package network.UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class EcoClient {
private DatagramSocket socket = null;
//服务器的IP和端口号
private int serverPort;
private String serverIp;
// 构造方法,通过port指定端口号
public EcoClient(int port,String ip) throws Exception{
//客户端这边不去指定端口号,不填写的话操作系统会随机分配一个空闲的端口号
socket = new DatagramSocket();
serverPort = port;
serverIp = ip;
}
public void start() throws IOException{
Scanner scanner = new Scanner(System.in);
System.out.println("客户端启动成功,端口号为:"+socket.getLocalPort());
while(true){
//1.从控制台读取用户输入的内容
System.out.print(">");
String request = scanner.next();
//2.构造UDP请求,并发送给服务器,不仅要填写内容,还要填写服务器端口号和IP
//通过InetAddress.getByName(serverIp)可以把服务器的IP字符串转换成一个InetAddress对象
DatagramPacket reqPacket = new DatagramPacket(
request.getBytes(),
request.getBytes().length,
InetAddress.getByName(serverIp),
serverPort
);
socket.send(reqPacket);
//3.读取服务器响应
DatagramPacket respPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(respPacket);
String response = new String(respPacket.getData(),0,respPacket.getLength());
//4.把响应显示到控制台
System.out.println(response);
}
}
public static void main(String[] args) throws Exception {
//127.0.0.1是回环地址,指的是本机,如果服务器和客户端在同一台机器上,就可以使用回环地址
EcoClient client = new EcoClient(9090,"127.0.0.1");
client.start();
}
}
可能大家会有点懵,一下子这么多内容,我们来拆开慢慢讲
1.服务器
1.创建DategrameSocket对象
我们需要先创建一个DategrameSocket对象,这样的DategrameSocket对象用来表示网卡,是连接客户端和服务器之间的信息通道
java
private DatagramSocket socket = null;
// 构造方法,通过port指定端口号
public EcoServer(int port) throws Exception{
socket = new DatagramSocket(port);
}
2.写启动服务器的run()方法
(1)服务器一启动,先一个打印语句,表示服务器可以正常启动
java
System.out.println("服务器启动成功,端口号为:"+socket.getLocalPort());
(2)由于服务器需要一直接受来自客户端发送的请求信息,所以使用while循环来一直运行
服务器需要先构造一个DategramePacket对象,用于接受客户端发送来的数据
在前面提及过,UDP协议中,数据报是收发数据的基本单位,而DategramePacket对象就表示一个数据报
java
DatagramPacket receivePacket = new DatagramPacket(new byte[1024],1024);
然后通过socket.receive()方法接收客户端发来的请求,里面的receivePacket是用来接收客户端数据内容,以及记录客户端的ip和端口号,以便于后面返回响应知道对方地址
java
socket.receive(receivePacket);
(3)解析数据
将接收到的数据,转换为字符串
java
String request = new String(receivePacket.getData(),0,receivePacket.getLength());
(4)计算响应
所谓响应就是服务器对客户端发来的请求所产生的返回
由于我们这边是回显服务器,所以直接返回即可
java
String response = process(request);
java
public String process(String request) {
return request;
}
(5)把响应写成一个DategramePacket,然后通过socket发送给客户端
这里的,getSocketAddress(),就是得到对方的IP和端口号
java
//(1)把响应先构造成一个DategramePacket对象
DatagramPacket responsePacket = new DatagramPacket(
response.getBytes(),
response.getBytes().length,
receivePacket.getSocketAddress()
);
//(2)通过send方法把响应发送给客户端
socket.send(responsePacket);
(6).打印日志,方便后续查看结果是否符合预期
java
System.out.printf("[%s:%d],req=%s,resp=%s\n",
receivePacket.getSocketAddress(),
receivePacket.getPort(),
request,
response
);
3.main方法创建EcoServer服务器,然后启动服务器
java
public static void main(String[] args) throws Exception {
//这里的9090是指的是服务器的端口号,每一个socket对象都会占用系统的一个端口号
EcoServer server = new EcoServer(9090);
server.run();
}
2.客户端
1.创建DategrameSocke对象
用于收发数据,同时这里需要填写服务器的IP和端口号
这里可能有人会疑惑,为什么在服务器创建DategrameSocket对象的时候,不需要填写IP和端口号.
这是因为服务器不能知道客户端的ip和端口,每个用户的ip和端口都是不确定的,所以服务器给客户端发送信息的时候都需要通过客户端发来的数据报里面的ip和端口,才能发送响应给用户
但是客户端不一样,服务器的ip和端口号是可以由程序员指定的,所以需要填写ip和端口号
java
private DatagramSocket socket = null;
//服务器的IP和端口号
private int serverPort;
private String serverIp;
// 构造方法,通过port指定端口号
public EcoClient(int port,String ip) throws Exception{
//客户端这边不去指定端口号,不填写的话操作系统会随机分配一个空闲的端口号
socket = new DatagramSocket();
serverPort = port;
serverIp = ip;
}
2.客户端start()方法
(1)由用户输入具体内容
java
System.out.print(">");
String request = scanner.next();
(2)构建数据报,将内容填入数据报并发送给服务器
java
DatagramPacket reqPacket = new DatagramPacket(
request.getBytes(),
request.getBytes().length,
InetAddress.getByName(serverIp),
serverPort
);
socket.send(reqPacket);
(3)读取服务器响应,然后显示响应
java
DatagramPacket respPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(respPacket);
String response = new String(respPacket.getData(),0,respPacket.getLength());
//4.把响应显示到控制台
System.out.println(response);
(4)启动客户端
java
public static void main(String[] args) throws Exception {
//127.0.0.1是回环地址,指的是本机,如果服务器和客户端在同一台机器上,就可以使用回环地址
EcoClient client = new EcoClient(9090,"127.0.0.1");
client.start();
}
3.字典服务器:
java
package network.UDP;
import java.util.HashMap;
public class DictServer extends EcoServer{
HashMap<String,String> dict = new HashMap<>();
public DictServer(int port) throws Exception {
super(port);
dict.put("hello","你好");
dict.put("world","世界");
dict.put("cat","猫");
dict.put("dog","狗");
dict.put("mouse","老鼠");
dict.put("tree","树");
dict.put("flower","花");
}
// 字典服务器
@Override
public String process(String request) {
return dict.getOrDefault(request,"没有这个单词");
}
public static void main(String[] args) throws Exception {
DictServer server = new DictServer(9090);
server.run();
}
}
主要原理很简单,就是通过子类继承父类的方式,重写process方法即可
四.TCP
关于TCP协议,使用的是Socket和ServerSocket这俩类
与UDP协议不同的是,TCP协议是有连接的,所以在进行网络通信的时候,需要先建立连接,这一步是UDP协议没有的
与此同时,TCP协议是使用字节流来传输信息的,而UDP协议传输信息的基本单位是数据报
这就意味着,UDP协议传输的内容大小有上限,而TCP协议传输的内容大小无上限,这一点会在后面网络原理具体带大家了解
1.服务器
先把服务器总代码放出来:
java
package network.TCP;
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class EcoServer {
private ServerSocket serverSocket;
public EcoServer(int port) throws IOException{
//建立连接
serverSocket = new ServerSocket(port);
}
public void start() throws IOException{
System.out.println("服务器启动");
ExecutorService threadpool = Executors.newCachedThreadPool();
while(true){
Socket clientSocket = serverSocket.accept();
//一个客户端情况
//process(clientSocket);
//当客户端数量超过1的时候,就会出现阻塞情况,可以使用多线程来解决这个问题
//为什么会出现阻塞情况,是因为在一个客户端没退出的情况下
//当前循环会一直卡在process这个方法这里不结束,导致无法接收到另一个客户端的socket对象
//可以使用多线程来解决这个问题
//每个客户端都创建一个线程来处理
// Thread thread = new Thread(()->{
// try{
// process(clientSocket);
// }catch(IOException e){
// e.printStackTrace();
// }
// });
// thread.start();
//还可以使用线程池
threadpool.execute(()->{
try{
process(clientSocket);
}catch(IOException e){
e.printStackTrace();
}
});
}
}
private void process(Socket socket) throws IOException {
try(
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
){
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
//读取客户请求
if(!scanner.hasNext()){
System.out.println("客户关闭连接");
break;
}
//读取客户请求
String request = scanner.next();
//计算响应
String response = calculate(request);
//将响应显示回客户端
printWriter.println(response);
//刷新缓存区
printWriter.flush();
System.out.printf("[%s:%d],req: %s,resp:%s \n",
socket.getInetAddress().toString(),
socket.getPort(),
request,
response);
}
}catch(IOException e){
e.printStackTrace();
}finally{
//最后关闭socket文件
try{
socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
private String calculate(String request) {
//回显服务器
return request;
}
public static void main(String[] args) {
try{
//创建服务器
EcoServer server = new EcoServer(8888);
//启动服务器
server.start();
}catch(IOException e){
e.printStackTrace();
}
}
}
接下来带大家拆分了解这个服务器代码的具体原理:
1.建立连接(与UDP协议不同之处)
创建ServerSocket对象,就是建立连接的过程
ServerSocket对象可以帮助我们绑定端口号,完成监听端口,等待用户连接的作用
这里提一嘴,为什么UDP协议没有这个过程?
打个比方,UDP协议就像是寄信这个过程,你只需要知道对方的地址就可以寄信,不需要对方同不同意(同不同意这一步就相当于是否建立连接)
而TCP协议就像是打电话,你知道了对方的电话号码,拨号过去之后,还需要对方选择是否接听
java
public EcoServer(int port) throws IOException{
//建立连接
serverSocket = new ServerSocket(port);
}
建立连接之后,我们需要使用accept()方法去接收连接,相当于是接听电话这个过程,会返回一个socket对象,这个socket对象就是我们后续与用户端进行网络通信的socket文件
java
Socket clientSocket = serverSocket.accept();
2.通过start()方法启动服务器
java
public void start() throws IOException{
System.out.println("服务器启动");
ExecutorService threadpool = Executors.newCachedThreadPool();
while(true){
Socket clientSocket = serverSocket.accept();
//一个客户端情况
//process(clientSocket);
//当客户端数量超过1的时候,就会出现阻塞情况,可以使用多线程来解决这个问题
//为什么会出现阻塞情况,是因为在一个客户端没退出的情况下
//当前循环会一直卡在process这个方法这里不结束,导致无法接收到另一个客户端的socket对象
//可以使用多线程来解决这个问题
//每个客户端都创建一个线程来处理
// Thread thread = new Thread(()->{
// try{
// process(clientSocket);
// }catch(IOException e){
// e.printStackTrace();
// }
// });
// thread.start();
//还可以使用线程池
threadpool.execute(()->{
try{
process(clientSocket);
}catch(IOException e){
e.printStackTrace();
}
});
}
}
这里有三种方式去完成一次通信任务
第一种就是通过主线程去完成process()方法 ,这样有个缺点,就是一次只能接受一个客户端发来的连接,当这个客户端不断开连接的话,就无法给其他客户端发送响应
第二种就是通过多线程方法,主线程负责去接收客户端发送来的请求,然后将完成process的这个过程通过创建一个线程的方式去完成.
第三种是通过线程池的方式,当客户端请求连接的数量过多的时候,频繁创建销毁线程就会产生不可忽视的资源开销,所以通过线程池的方式,来减少这种资源开销.
3.通过process()方法完成一次通信的过程
java
try(
//可以直接通过try-with-resources来关闭socket
//socket;
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
)
先通过try with resource语法来创建流对象,这种语法的优点就是可以省去手动关闭流对象这个步骤,同理,我们知道socket文件在使用之后也是需要去关闭的,我们也可以将socket对象放在try()括号里面
再通过Scanner和PrintWriter来读写字节流,我们知道InputStream和OutputStream很难用,需要创建数组然后再循环读取,所以有了Scanner和PrintWriter包装,可以直接通过字符串的方式来读写

java
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
当前代码是服务器代码,所以这里面scanner是否有内容表示的就是客户端是否发送请求,而printWriter是服务器需要发送的响应
通过while(true)循环,来一直读取是否有请求,然后判断scanner.hasNext()来看客户是否关闭连接
java
if(!scanner.hasNext()){
System.out.println("客户关闭连接");
break;
}
scanner.hasNext()是如何知道客户端是断开连接还是没有输入呢?


当客户端有请求发送来的时候,就读取请求,然后计算响应,发送响应到客户端,最后打印日志,这一块步骤和UDP协议类似,唯二不同的是,TCP协议有一个刷新缓冲区的步骤和不需要手动填入客户端的ip和端口号
什么是缓冲区?就是临时存放数据的内存.
当我们使用writer.println()的时候,并没有真正的将信息发送出去,而是将信息保存在了缓冲区里面,当信心量达到一定程度,才会一次性发送出去
就好比喝水,我们不会一直对着水龙头喝水,而是将水接到水杯里面,有一定量的水才会去喝
那为什么需要缓冲区呢?
这是因为两个设备的速度不一样,CPU速度快,网卡速度慢,如果没有缓冲区的存在,CPU必须等网卡,效率极低,所以需要缓冲区作为过渡.
所谓的刷新缓存区,就是将缓存区里面的内容真正发送出去.

至于为什么在TCP协议中不需要手动填入客户端的IP和端口号,这是因为在之前我们已经建立连接了.
java
//读取客户请求
String request = scanner.next();
//计算响应
String response = calculate(request);
//将响应显示回客户端
printWriter.println(response);
//刷新缓存区
printWriter.flush();
System.out.printf("[%s:%d],req: %s,resp:%s \n",
socket.getInetAddress().toString(),
socket.getPort(),
request,
response);
private String calculate(String request) {
//回显服务器
return request;
}
最后要记得关闭socket对象,防止内存资源泄露,文件描述符表耗尽,客户端一直占着连接和端口资源占用这些问题,当然,如果我们在try()里面写入了socket;就不需要手动关闭
java
finally{
//最后关闭socket文件
try{
socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
4.通过main()创建客户端对象
java
public static void main(String[] args) {
try{
//创建服务器
EcoServer server = new EcoServer(8888);
//启动服务器
server.start();
}catch(IOException e){
e.printStackTrace();
}
}
2.客户端
先把客户端总代码放出来:
java
package network.TCP;
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 EcoClient {
private Socket socket;
public EcoClient(String serverIP,int port) throws IOException{
//建立连接
socket = new Socket(serverIP,port);
}
public void start() throws IOException{
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
Scanner scannerNext = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
){
while(true){
//循环从控制台读取请求
System.out.print(">");
String request = scanner.nextLine();
//将请求发送给服务器
printWriter.println(request);
//刷新缓存区
printWriter.flush();
//读取服务器响应
if(!scannerNext.hasNext()){
System.out.println("客户关闭连接");
break;
}
String response = scannerNext.next();
//打印服务器响应
System.out.println(response);
}
}catch(IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
try{
//创建客户端
EcoClient client = new EcoClient("127.0.0.1",8888);
//启动客户端
client.start();
}catch(IOException e){
e.printStackTrace();
}
}
}
1.建立与服务器的连接
java
public class EcoClient {
private Socket socket;
public EcoClient(String serverIP,int port) throws IOException{
//建立连接
socket = new Socket(serverIP,port);
}
2.通过字节流写入和发送请求
java
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
Scanner scannerNext = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
)
这里我们要注意的是,先通过字节流拿到socket的读写功能,然后再通过Scanner和PrintWriter包装,更好的使用字节流
3.发送请求读取响应打印日志
这些跟服务器的操作类似,就不一一介绍了
java
while(true){
//循环从控制台读取请求
System.out.print(">");
String request = scanner.nextLine();
//将请求发送给服务器
printWriter.println(request);
//刷新缓存区
printWriter.flush();
//读取服务器响应
if(!scannerNext.hasNext()){
System.out.println("客户关闭连接");
break;
}
String response = scannerNext.next();
//打印服务器响应
System.out.println(response);
}
4.main()创建客户端对象
java
public static void main(String[] args) {
try{
//创建客户端
EcoClient client = new EcoClient("127.0.0.1",8888);
//启动客户端
client.start();
}catch(IOException e){
e.printStackTrace();
}
}
到此,我们关于TCP和UDP协议的使用,就告一段落了,下面我会为大家讲解有关网络原理的相关知识