6.实现响应404页面

上一个版本中我们已经实现了根据浏览器中用户在地址栏上输入的URL中的抽象路径去
static目录下寻找对应资源进行响应的工作。

但是会存在路径输入有误,导致定位不对(要么定位的是一个目录,要么该文件不存在)
此时再发送响应的响应正文时使用文件输入流读取就会出现异常提示该资源不存在。

这是一个典型的404情况,因此我们在ClientHandler处理请求的环节,在实例化File
对象根据抽象路径定位webapps下的资源后,要添加一个分支,若该资源存在则将其响应
回去,如果不存在则要响应404状态代码和404页面提示用户

  • 状态行中的状态代码改为404,状态描述改为NotFound
  • 响应头Content-Length发送的是404页面的长度
  • 响应正文为404页面内容
File file = new File(staticDir,path);System.out.println("该页面是否存在:"+file.exists());String line;if(file.isFile()){//用户请求的资源在static目录下存在且是一个文件line = "HTTP/1.1 200 OK";}else{line = "HTTP/1.1 404 NotFound";file = new File(staticDir,"/root/404.html");}

7.重构ClientHandler中发送响应的工作拆分出去

类似于之前重构request请求, 默认是200OK, 404对状态行重新赋值更加合理

  • 在com.webserver.http包中新建类:HttpServletResponse 响应对象
  • 在响应对象中定义对应的属性来保存响应内容并定义response方法来发送响应
  • 修改ClientHandler,使用响应对象完整响应的发送

HttpServletResponse

public class HttpServletResponse {private Socket socket;//状态行的相关信息private int statusCode = 200;           //状态代码private String statusReason = "OK";     //状态描述//响应头相关信息//响应正文相关信息private File contentFile; //正文对应的实体文件public HttpServletResponse(Socket socket){this.socket = socket;}/*** 将当前响应对象内容以标准的HTTP响应格式,发送给客户端(浏览器)*/public void response() throws IOException {//3.1发送状态行sendStatusLine();//3.2发送响应头sendHeaders();//3.3发送响应正文sendContent();}//发送状态行private void sendStatusLine() throws IOException {println("HTTP/1.1" + " " + statusCode + " " +statusReason);}//发送响应头private void sendHeaders() throws IOException {println("Content-Type: text/html");println("Content-Length: "+contentFile.length());//单独发送一组回车+换行表示响应头部分发送完了!println("");}//发送响应正文private void sendContent() throws IOException {try(FileInputStream fis = new FileInputStream(contentFile);) {OutputStream out = socket.getOutputStream();int len;byte[] data = new byte[1024 * 10];while ((len = fis.read(data)) != -1) {out.write(data, 0, len);}}}/*** 向浏览器发送一行字符串(自动补充CR+LF)* @param line*/private void println(String line) throws IOException {OutputStream out = socket.getOutputStream();out.write(line.getBytes(StandardCharsets.ISO_8859_1));out.write(13);//发送回车符out.write(10);//发送换行符}public int getStatusCode() {return statusCode;}public void setStatusCode(int statusCode) {this.statusCode = statusCode;}public String getStatusReason() {return statusReason;}public void setStatusReason(String statusReason) {this.statusReason = statusReason;}public File getContentFile() {return contentFile;}public void setContentFile(File contentFile) {this.contentFile = contentFile;}
}

ClientHandler

public class ClientHandler implements Runnable {private Socket socket;public ClientHandler(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {//1解析请求HttpServletRequest request = new HttpServletRequest(socket);HttpServletResponse response = new HttpServletResponse(socket);//2处理请求//定位到:target/classesFile rootDir = new File(ClientHandler.class.getClassLoader().getResource(".").toURI());//定位static目录File staticDir = new File(rootDir,"static");String path = request.getUri();File file = new File(staticDir,path);System.out.println("该页面是否存在:"+file.exists());if(file.isFile()){//用户请求的资源在static目录下存在且是一个文件response.setContentFile(file);}else{response.setStatusCode(404);response.setStatusReason("NotFound");response.setContentFile(new File(staticDir,"/root/404.html"));}//3发送响应response.response();} catch (IOException | URISyntaxException e) {e.printStackTrace();} finally {//按照HTTP协议要求,一次交互后断开TCP链接try {socket.close();} catch (IOException e) {e.printStackTrace();}}}
}

8.重构ClientHandler中处理请求部分为DispatchServlet

Dispatchervlet:使用到了单例模式

  • 在com.webserver.core包下新建类:DispatcherServlet  并定义service方法,用来处理请求
  • 将ClientHandler处理请求的操作移动到service方法中去
  • ClientHandler通过调用DispatcherServlet的service完成处理请求环节
public class DispatcherServlet {private static DispatcherServlet servlet;private static File rootDir;private static File staticDir;static {servlet = new DispatcherServlet();try {//定位到:target/classesrootDir = new File(ClientHandler.class.getClassLoader().getResource(".").toURI());//定位static目录staticDir = new File(rootDir,"static");} catch (URISyntaxException e) {e.printStackTrace();}}private DispatcherServlet(){}public void service(HttpServletRequest request, HttpServletResponse response){String path = request.getUri();File file = new File(staticDir,path);System.out.println("该页面是否存在:"+file.exists());if(file.isFile()){//用户请求的资源在static目录下存在且是一个文件response.setContentFile(file);}else{response.setStatusCode(404);response.setStatusReason("NotFound");response.setContentFile(new File(staticDir,"/root/404.html"));}}public static DispatcherServlet getInstance(){return servlet;}
}

9.重构HttpServletResponse可以根据设置发送响应头

访问学子商城webapp资源后访问其首页,发现页面无法正常显示.
浏览器F12跟踪请求和响应的交互发现两个问题:
1:我们仅发送了两个响应头(Content-Length和Content-Type).
  虽然目前仅需要这两个头,但是服务端实际可以根据处理情况设置需要发送其他响应头
2:Content-Type的值是固定的"text/html",这导致浏览器请求到该资源后无法正确
  理解该资源因此没有发挥出实际作用.分两个版本解决这两个问题
此版本解决响应头可根据设置进行发送

public class HttpServletResponse {private Socket socket;//状态行的相关信息private int statusCode = 200;           //状态代码private String statusReason = "OK";     //状态描述//响应头相关信息//响应正文相关信息private File contentFile; //正文对应的实体文件public HttpServletResponse(Socket socket){this.socket = socket;}/*** 将当前响应对象内容以标准的HTTP响应格式,发送给客户端(浏览器)*/public void response() throws IOException {//3.1发送状态行sendStatusLine();//3.2发送响应头sendHeaders();//3.3发送响应正文sendContent();}//发送状态行private void sendStatusLine() throws IOException {println("HTTP/1.1" + " " + statusCode + " " +statusReason);}//发送响应头private void sendHeaders() throws IOException {println("Content-Type: text/html");println("Content-Length: "+contentFile.length());//单独发送一组回车+换行表示响应头部分发送完了!println("");}//发送响应正文private void sendContent() throws IOException {try(FileInputStream fis = new FileInputStream(contentFile);) {OutputStream out = socket.getOutputStream();int len;byte[] data = new byte[1024 * 10];while ((len = fis.read(data)) != -1) {out.write(data, 0, len);}}}/*** 向浏览器发送一行字符串(自动补充CR+LF)*/private void println(String line) throws IOException {OutputStream out = socket.getOutputStream();out.write(line.getBytes(StandardCharsets.ISO_8859_1));out.write(13);//发送回车符out.write(10);//发送换行符}public int getStatusCode() {return statusCode;}public void setStatusCode(int statusCode) {this.statusCode = statusCode;}public String getStatusReason() {return statusReason;}public void setStatusReason(String statusReason) {this.statusReason = statusReason;}public File getContentFile() {return contentFile;}public void setContentFile(File contentFile) {this.contentFile = contentFile;}
}

10.继续重构HttpServletResponse

实现HttpServletResponse响应正确的MIME类型,即:Content-Type的值
这里我们使用java.nio.file.Files这个类来完成这个功能。这样一来,服务端就可以正确响应浏览器请求的任何资源了,使得浏览器可以正确显示内容. 注意实际上就算resopnse不发送Content-Type响应给浏览器, 浏览器也能分析大部分请求的得到的响应的类型,但是有的请求是没有后缀的,因此还是需要在服务器端响应一个Content-Type给到浏览器

public void setContentFile(File contentFile) throws IOException {this.contentFile = contentFile;String contentType = Files.probeContentType(contentFile.toPath());//如果根据文件没有分析出Content-Type的值就不添加这个头了,HTTP协议规定服务端不发送这个头时由浏览器自行判断类型if(contentType!=null){addHeader("Content-Type",contentType);}addHeader("Content-Length",contentFile.length()+"");}

11解决空请求异常问题

HTTP协议注明:为了保证服务端的健壮性,应当忽略客户端空的请求
浏览器有时会发送空请求,即:与服务端链接后没有发送标准的HTTP请求内容,直接与服务端断开链接。此时服务端按照一问一答的处理流程在解析请求时请求行由于没有内容,在拆分后获取信息会出现数组下标越界

  • 在com.webserver.http包下新建自定义异常:EmptyRequestException,空请求异常
  • 在HttpServletRequest的解析请求行方法parseRequestLine中,当读取请求行后发现是一个
      空字符串则对外抛出空请求异常并通过构造方法继续对外抛出给ClientHandler
  • ClientHandler添加一个新的catch专门捕获空请求异常,捕获后无需做任何处理,目的仅仅是
      忽略处理请求和响应客户端的工作

HttpServletRequest

//解析请求行private void parseRequestLine() throws IOException, EmptyRequestException {String line = readLine();if(line.isEmpty()){//如果请求行为空字符串,则说明本次为空请求!throw new EmptyRequestException();}System.out.println("请求行:"+line);String[] data = line.split("\\s");method = data[0];uri = data[1];protocol = data[2];}

ClientHandler

} catch (IOException e) {e.printStackTrace();} catch (EmptyRequestException e) {} finally {//按照HTTP协议要求,一次交互后断开TCP链接try {socket.close();} catch (IOException e) {e.printStackTrace();}}

12.实现对于非静态资源请求的解析

  • 页面如何将用户输入的信息提交给服务端表单form的使用
  • 服务端如何通过解析请求得到表单数据
  • DispatcherServlet如何区分请求是处理注册还是请求一个静态资源(页面,图片等)

重构HttpServletRequest

//解析请求行private void parseRequestLine() throws IOException, EmptyRequestException {String line = readLine();if (line.isEmpty()) {//如果请求行为空字符串,则说明本次为空请求!throw new EmptyRequestException();}System.out.println("请求行:" + line);String[] data = line.split("\\s");method = data[0];uri = data[1];protocol = data[2];parseURI();//进一步解析uri}//进一步解析uriprivate void parseURI() {/*uri有两种情况:1:不含有参数的例如: /index.html直接将uri的值赋值给requestURI即可.2:含有参数的例如:/regUser?username=fancq&password=&nickname=chuanqi&age=22将uri中"?"左侧的请求部分赋值给requestURI将uri中"?"右侧的参数部分赋值给queryString将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值并将参数名作为key,参数值作为value存入到parameters中。*/String[] data = uri.split("\\?");requestURI = data[0];if(data.length>1){//说明?后面还有参数queryString = data[1];data = queryString.split("&");//将参数部分按照"&"拆分出每一组参数for(String para : data){//para: username=zhangsanString[] paras = para.split("=");parameters.put(paras[0],paras.length>1?paras[1]:"");}}System.out.println("requestURI:"+requestURI);System.out.println("queryString:"+queryString);System.out.println("parameters:"+parameters);}

重构DispatcherServlet,使得能够区分业务请求还是静态资源请求

public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {//不能直接使用uri作为请求路径处理了,因为可能包含参数,而参数内容不是固定信息。String path = request.getRequestURI();//判断本次请求是否为请求某个业务if("/regUser".equals(path)){//如果和注册页面form中action一致,则处理注册UserController controller = new UserController();controller.reg(request,response);}else {File file = new File(staticDir, path);System.out.println("该页面是否存在:" + file.exists());if (file.isFile()) {//用户请求的资源在static目录下存在且是一个文件response.setContentFile(file);} else {response.setStatusCode(404);response.setStatusReason("NotFound");response.setContentFile(new File(staticDir, "/root/404.html"));}}//测试添加其它响应头response.addHeader("Server","WebServer");}

13.实现重定向

当浏览器提交一个请求时,比如注册,那么请求会带着表单信息请求注册功能。而注册功能处理
完毕后直接设置一个页面给浏览器,这个过程是内部跳转。
即:浏览器上的地址栏中地址显示的是提交表单的请求,而实际看到的是注册结果的提示页面
这有一个问题,如果此时浏览器刷新,会重复上一次的请求,即:再次提交表单请求注册业务。这种情况会造成数据库压力增大,  为了解决这个问题,我们可以使用重定向, 避免这种内部跳转恶意刷新带来数据库压力问题

实现:
在HttpServletResponse中定义一个方法:sendRedirect()
该方法中设置状态代码为302,并在响应头中包含Location指定需要浏览器重新发起请求的路径, 将原来Controller中内部跳转页面的操作全部改为重定向。

注意,重定向这里传的参数Path必须是/开头,因为这是给浏览器解析的,而不是JVM,对于浏览器来说./和/和不加/代表的抽象路径第几个/是不同的

重定向的响应格式示例:
HTTP/1.1 302 Moved Temporarily
Location: /have_user.html

HttpServletResponse

public void sendRedirect(String path){statusCode = 302;statusReason = "Moved Temporarily";addHeader("Location",path);}

UserController

public class UserController {private static File userDir;//该目录用于保存所有注册用户文件(一堆的.obj文件)static{userDir = new File("./users");if(!userDir.exists()){//如果该目录不存在userDir.mkdirs();}}public void reg(HttpServletRequest request, HttpServletResponse response){System.out.println("开始处理用户注册!!!!!!!!!!!!!!!!!!!");//对应reg.html页面表单中<input name="username" type="text">String username = request.getParameter("username");String password = request.getParameter("password");String nickname = request.getParameter("nickname");String age = request.getParameter("age");System.out.println(username+","+password+","+nickname+","+age);//对数据进行必要的验证工作if(username.isEmpty()||password.isEmpty()||nickname.isEmpty()||age.isEmpty()||!age.matches("[0-9]+")){//如果如何上述情况,则直接响应给用户一个注册失败提示页面,告知信息输入有误。response.sendRedirect("/reg_info_error.html");return;}//处理注册//将年龄转换为int值int age_ = Integer.parseInt(age);User user = new User(username,password,nickname,age_);//参数1:当前File表示的文件所在的目录  参数2:当前文件的名字File userFile = new File(userDir,username+".obj");if(userFile.exists()){//文件已经存在说明该用户已经存在了!response.sendRedirect("/have_user.html");return;}try (FileOutputStream fos = new FileOutputStream(userFile);ObjectOutputStream oos = new ObjectOutputStream(fos);){oos.writeObject(user);//响应注册成功页面给浏览器response.sendRedirect("/reg_success.html");} catch (IOException e) {e.printStackTrace();}}
}

14.实现支持Post请求

当页面form表单中包含用户隐私信息或有附件上传时,应当使用POST形式提交
POST会将表单数据包含在请求的消息正文中。
如果表单中没有附件,则正文中包含的表单数据就是一个字符串,而格式就是原GET形式
提交时抽象路径中"?"右侧的内容

实现:
1:完成HttpServletRequest中的解析消息正文的方法parseContent, 服务端为了保证兼容性, 统一用小写
 当页面(reg.html或login.html)中form的提交方式改为POST时,表单数据被包含在正文里,并且
 请求的消息头中会出现Content-Type和Content-Length用于告知服务端正文内容。因此我们可以根据它们来解析正文。重构HttpServletRequest类,编写parseContent方法.

public class HttpServletRequest {private Socket socket;//请求行相关信息private String method;  //请求方式private String uri;     //抽象路径private String protocol;//协议版本private String requestURI;//保存uri中的请求部分("?"左侧的内容)private String queryString;//保存uri中的参数部分("?"右侧的内容)private Map<String, String> parameters = new HashMap<>();//保存每一组参数//消息头相关信息private Map<String, String> headers = new HashMap<>();public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {this.socket = socket;//1.1解析请求行parseRequestLine();//1.2:解析消息头parseHeaders();//1.3:解析消息正文parseContent();}//解析请求行private void parseRequestLine() throws IOException, EmptyRequestException {String line = readLine();if (line.isEmpty()) {//如果请求行为空字符串,则说明本次为空请求!throw new EmptyRequestException();}System.out.println("请求行:" + line);String[] data = line.split("\\s");method = data[0];uri = data[1];protocol = data[2];parseURI();//进一步解析uri}//进一步解析uriprivate void parseURI() {/*uri有两种情况:1:不含有参数的例如: /index.html直接将uri的值赋值给requestURI即可.2:含有参数的例如:/regUser?username=fancq&password=&nickname=chuanqi&age=22将uri中"?"左侧的请求部分赋值给requestURI将uri中"?"右侧的参数部分赋值给queryString将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值并将参数名作为key,参数值作为value存入到parameters中。*/String[] data = uri.split("\\?");requestURI = data[0];if(data.length>1){//说明?后面还有参数queryString = data[1];parseParameters(queryString);}System.out.println("requestURI:"+requestURI);System.out.println("queryString:"+queryString);System.out.println("parameters:"+parameters);}/*解析参数,因为参数GET请求来自抽象路径的"?"右侧,而POST请求来自消息正文,因为格式一致,所以重用解析操作*/private void parseParameters(String line){String[] data = line.split("&");//将参数部分按照"&"拆分出每一组参数for(String para : data){//para: username=zhangsanString[] paras = para.split("=");parameters.put(paras[0],paras.length>1?paras[1]:"");}}//解析消息头private void parseHeaders() throws IOException {while (true) {String line = readLine();if (line.isEmpty()) {//如果readLine返回空字符串,说明单独读取到了回车+换行break;}System.out.println("消息头:" + line);/*将每一个消息头按照": "(冒号+空格拆)分为消息头的名字和消息头的值并以key,value的形式存入到headers中*/String[] data = line.split(":\\s");//将消息头的名字转换为全小写后存入headers,兼容性更好(浏览器发送的消息头无论大小写,只要拼写正确即可)headers.put(data[0].toLowerCase(Locale.ROOT), data[1]);}//while循环结束,消息头解析完毕System.out.println("headers:" + headers);}//解析消息正文private void parseContent() throws IOException {//判断本次请求方式是否为post请求if("POST".equalsIgnoreCase(method)){//根据消息头Content-Length来确定正文的字节数量以便进行读取String contentLength = getHeader("Content-Length");if(contentLength!=null){//判断不为null的目的是确保有消息头Content-Lengthint length = Integer.parseInt(contentLength);System.out.println("正文长度:"+length);byte[] data = new byte[length];InputStream in = socket.getInputStream();in.read(data);//根据Content-Type来判断正文类型,并进行对应的处理String contentType = getHeader("Content-Type");//分支判断不同的类型进行不同的处理if("application/x-www-form-urlencoded".equals(contentType)){//判断类型是否为form表单不带附件的数据//该类型的正文就是一行字符串,就是原GET请求提交表单是抽象路径中"?"右侧的参数String line = new String(data, StandardCharsets.ISO_8859_1);System.out.println("正文内容:"+line);parseParameters(line);}
//                else if(){}//扩展其它类型并进行对应的处理}}}private String readLine() throws IOException {//通常被重用的代码都不自己处理异常//同一个socket对象无论调用多少次getInputStream()获取的始终是同一个输入流InputStream in = socket.getInputStream();int d;//每次读取到的字节char cur = 'a', pre = 'a';//cur表示本次读取到的字符,pre表示上次读取到的字符StringBuilder builder = new StringBuilder();while ((d = in.read()) != -1) {cur = (char) d;if (pre == 13 && cur == 10) {//是否已经连续读取到了回车+换行符break;}builder.append(cur);pre = cur;}return builder.toString().trim();}public String getMethod() {return method;}public String getUri() {return uri;}public String getProtocol() {return protocol;}public String getHeader(String name) {/*headers:key             valuecontent-type    xxx/xxx*/return headers.get(name.toLowerCase(Locale.ROOT));}public String getRequestURI() {return requestURI;}public String getQueryString() {return queryString;}public String getParameter(String name) {return parameters.get(name);}
}

手写Web服务器(二)相关推荐

  1. 手写Web服务器(三)

    15.解决浏览器传递中文问题 由于HTTP协议只支持ISO8859-1, 此字符集不支持中文, 导致请求中包含中文时, 无法正确传递中文. 在UTF-8中每个中文用三个字节表示, 一个方案是在请求前, ...

  2. 手写webserver服务器

    手写webserver服务器 文章目录 手写webserver服务器 前言 一.web server执行流程 组件说明 项目地址 二.代码实现 三. 效果展示 四.总结 前言 webserver 服务 ...

  3. 【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)

    [学习笔记]Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程.手写 Promise(课前准备) [学习笔记]Part1·JavaScript·深度剖析-函数式编程与 JS 异步 ...

  4. Golang之手写web框架

    Go手写Web框架 1.1 标准启动方式 通过定义接口,使用 net/http 库封装基础的功能,通过自定义函数的方式可以自定义 StandardStart.go // Handler 用于实现处理器 ...

  5. 自己动手写web服务器一(浏览器的访问信息)

    要协议一个web服务器,需要了解http协议,下面我们来看一下当浏览器请求网张的时候向web服务器发送的数据,我使用的是ubuntu 中telent展现一个下过程.我需要一个简单的网站来演示一下,我装 ...

  6. 手写数字图片二值化转换为32*32数组。

    最近课设外加生病,本来打算在上一篇机器学习使用k-近邻算法改进约会网站的配对效果.就打算写的一直没有时间.按照<机器学习实战>的流程,手写数字识别是kNN中的最后一部分,也是一个比较经典的 ...

  7. android app用百度ocr识别sdk实现手写扫描功能(二)

    目录 1.概述 2.百度ocr手写识别核心功能实现 2.1 新建项目引用手写文字识别功能

  8. c#手写web内网网站映射,穿透服务器(一)

    本人一直做java方面开发已有六年,最近闲来无事捡起以前的c#知识写了个web网站内网映射软件. 一 .首先准备工作: 1.了解http通信协议,http/1.0及http/1.1 主要了解请求头,请 ...

  9. 利用html 5 websocket做个山寨版web聊天室(手写C#服务器)

    在之前的博客中提到过看到html5 的websocket后很感兴趣,终于可以摆脱长轮询(websocket之前的实现方式可以看看Developer Works上的一篇文章,有简单提到,同时也说了web ...

最新文章

  1. Extreme交换机基础配置命令
  2. Java Project和Web Project
  3. 国内首家,每周到岗上班3天,携程率先推出“3+2”混合办公模式
  4. 【splay】hdu 4453 2012杭州赛区A题
  5. 年入10亿,“山寨”耳机芯片凶猛
  6. windows 覆盖linux,您是否曾考虑过用Linux替换Windows?
  7. JAVA核心:内存、比较和Final
  8. 靠着零代码报表工具,转行报表开发后月薪超过3万
  9. php mysql_affected_rows获取sql执行影响的行数
  10. 简单的制作一个动态链接库(DLL)
  11. C# 输入选择文件夹
  12. 《CCNA路由和交换(200-120)学习指南》——2.4节认证提要
  13. SpringBoot学习(三)YAML语法、JSR303校验、多环境开发切换
  14. Python 高性能编程
  15. 清洁机器人--屏幕显示LCD方案之MCU SPI 接口驱动ST7789 LCD显示
  16. phpnow mysql升级_PHPNOW如何升级PHP
  17. excel复制公式递增_快速向下复制Excel公式
  18. ANC主动降噪蓝牙耳机南A2测评:日常通勤降噪亲民之选!
  19. 2020年Android GMS 认证 boot logo 最新要求
  20. LCM5369 降压控制器 P2P替代TPS536C9

热门文章

  1. 使用反向代理规避备案风险
  2. java 小项目:简单扑克牌游戏
  3. disable-output-escaping
  4. 修改Oracle默认端口1521
  5. M3中断向量表与A7中断向量表对比
  6. 某宝登录滑块拖动没反应解决,亲测有效
  7. OLAP(业务)—事务分析(查询)
  8. python实现美空图片抓取机器人
  9. SqlServer替换回车键和换行
  10. oracle drop index pk,oracle – 执行唯一/主键 – 删除索引