Tomcat是基于Sun公司标准的开源Servlet容器。

Servlet是什么?

Servlet(Server Applet),全称Java Servlet,未有中文译文。是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。

Servlet运行于支持Java的应用服务器中。从实现上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。

最早支持Servlet标准的是JavaSoft的Java Web Server。此后,一些其它的基于Java的Web服务器开始支持标准的Servlet。

Servlet编程需要使用到javax.servlet 和 javax.servlet.http两个包下面的类和接口,在所有的类和接口中,javax.servlet.servlet 接口最为重要。所有的servlet程序都必须实现该接口或者继承实现了该接口的类。

Servlet只是一种标准,按照这种标准既可以实现HTTP请求也可以实现其他的协议,用于执行Servlet的启动和管理程序称为Servlet容器,Tomcat便是一个很经常使用的Servlet容器。

上面一小节介绍了一个简单的HTTP的web服务器,是单线程的只能处理单个请求,请求处理完毕之后就重新开始准备接受连接的web服务器。

没有用到Servlet标准,只是简单的定义了Request和Response,简单解析了url以及将静态的文本文件通过输出流输出,并且我们发现response并没有http报文头,浏览器应该是针对这种情况做了特殊处理,比如直接打印流中的内容。

------------------------------------------------------------------------------------

一个功能齐全的Servlet容器有以下几件事情

1)第一次调用某个Servlet时,要载入该Servlet,并调用其init()方法,并且仅此一次

------------------------------------------------------------

下面举一个很简单的Servlet容器的例子

Constants.java

import java.io.File;
//定义了访问静态资源的位置
public class Constants
{
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
}

Constants类定义了静态finla WEB_ROOT,WEB_ROOT指定了该Servlet容器中使用PrimitiveServlet类和静态资源的位置。

HttpServer1一直等待HTTP请求,并且是单线程处理HTTP请求,直到等待到shut命令

与一种不同的是,此时将会在HttpServer1当中解析url,通过url判断寻找的是Servlet对象还是静态资源,然后分发到不同的处理过程。

都分别架构Request对象和Response对象传入

ServletProcessor1 和
StaticResourceProcessor的process方法当中,在方法当中回调Request和Response的相关方法

HttpServer1.java

import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException; public class HttpServer1
{ /**
* WEB_ROOT is the directory where our HTML and other files reside. For this
* package, WEB_ROOT is the "webroot" directory under the working directory.
* The working directory is the location in the file system from where the
* java command was invoked.
*/
// shutdown command
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; // the shutdown command received
private boolean shutdown = false; public static void main(String[] args)
{
HttpServer1 server = new HttpServer1();
server.await();
} public void await()
{
ServerSocket serverSocket = null;
int port = 8080;
try
{
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
} catch (IOException e)
{
e.printStackTrace();
System.exit(1);
} // Loop waiting for a request
while (!shutdown)
{
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try
{
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream(); // create Request object and parse
Request request = new Request(input);
request.parse(); // create Response object
Response response = new Response(output);
response.setRequest(request); // check if this is a request for a servlet or a static resource
// a request for a servlet begins with "/servlet/"
if (request.getUri().startsWith("/servlet/"))
{
ServletProcessor1 processor = new ServletProcessor1();
processor.process(request, response);
} else
{
StaticResourceProcessor processor = new StaticResourceProcessor();
processor.process(request, response);
} // Close the socket
socket.close();
// check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e)
{
e.printStackTrace();
System.exit(1);
}
}
}
}

Request.java

import java.io.InputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map; import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; public class Request implements ServletRequest
{ private InputStream input;
private String uri; public Request(InputStream input)
{
this.input = input;
} public String getUri()
{
return uri;
} private String parseUri(String requestString)
{
int index1, index2;
index1 = requestString.indexOf(' ');
if (index1 != -1)
{
index2 = requestString.indexOf(' ', index1 + 1);
if (index2 > index1)
return requestString.substring(index1 + 1, index2);
}
return null;
} public void parse()
{
// Read a set of characters from the socket
StringBuffer request = new StringBuffer(2048);
int i;
byte[] buffer = new byte[2048];
try
{
i = input.read(buffer);
} catch (IOException e)
{
e.printStackTrace();
i = -1;
}
for (int j = 0; j < i; j++)
{
request.append((char) buffer[j]);
}
System.out.print(request.toString());
uri = parseUri(request.toString());
} /* implementation of the ServletRequest */
public Object getAttribute(String attribute)
{
return null;
} public Enumeration getAttributeNames()
{
return null;
} public String getRealPath(String path)
{
return null;
} public RequestDispatcher getRequestDispatcher(String path)
{
return null;
} public boolean isSecure()
{
return false;
} public String getCharacterEncoding()
{
return null;
} public int getContentLength()
{
return 0;
} public String getContentType()
{
return null;
} public ServletInputStream getInputStream() throws IOException
{
return null;
} public Locale getLocale()
{
return null;
} public Enumeration getLocales()
{
return null;
} public String getParameter(String name)
{
return null;
} public Map getParameterMap()
{
return null;
} public Enumeration getParameterNames()
{
return null;
} public String[] getParameterValues(String parameter)
{
return null;
} public String getProtocol()
{
return null;
} public BufferedReader getReader() throws IOException
{
return null;
} public String getRemoteAddr()
{
return null;
} public String getRemoteHost()
{
return null;
} public String getScheme()
{
return null;
} public String getServerName()
{
return null;
} public int getServerPort()
{
return 0;
} public void removeAttribute(String attribute)
{
} public void setAttribute(String key, Object value)
{
} public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException
{
} @Override
public int getRemotePort()
{
// TODO Auto-generated method stub
return 0;
} @Override
public String getLocalName()
{
// TODO Auto-generated method stub
return null;
} @Override
public String getLocalAddr()
{
// TODO Auto-generated method stub
return null;
} @Override
public int getLocalPort()
{
// TODO Auto-generated method stub
return 0;
} @Override
public ServletContext getServletContext()
{
// TODO Auto-generated method stub
return null;
} @Override
public AsyncContext startAsync()
{
// TODO Auto-generated method stub
return null;
} @Override
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse)
{
// TODO Auto-generated method stub
return null;
} @Override
public boolean isAsyncStarted()
{
// TODO Auto-generated method stub
return false;
} @Override
public boolean isAsyncSupported()
{
// TODO Auto-generated method stub
return false;
} @Override
public AsyncContext getAsyncContext()
{
// TODO Auto-generated method stub
return null;
} @Override
public DispatcherType getDispatcherType()
{
// TODO Auto-generated method stub
return null;
} }

Response.java

import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.File;
import java.io.PrintWriter;
import java.util.Locale;
import javax.servlet.ServletResponse;
import javax.servlet.ServletOutputStream; public class Response implements ServletResponse
{ private static final int BUFFER_SIZE = 1024;
Request request;
OutputStream output;
PrintWriter writer; public Response(OutputStream output)
{
this.output = output;
} public void setRequest(Request request)
{
this.request = request;
} /* This method is used to serve a static page */
public void sendStaticResource() throws IOException
{
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try
{
/* request.getUri has been replaced by request.getRequestURI */
File file = new File(Constants.WEB_ROOT, request.getUri());
fis = new FileInputStream(file);
/*
* HTTP Response = Status-Line(( general-header | response-header |
* entity-header ) CRLF) CRLF [ message-body ] Status-Line =
* HTTP-Version SP Status-Code SP Reason-Phrase CRLF
*/
int ch = fis.read(bytes, 0, BUFFER_SIZE);
while (ch != -1)
{
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
} catch (FileNotFoundException e)
{
String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());
} finally
{
if (fis != null)
fis.close();
}
} /** implementation of ServletResponse */
public void flushBuffer() throws IOException
{
} public int getBufferSize()
{
return 0;
} public String getCharacterEncoding()
{
return null;
} public Locale getLocale()
{
return null;
} public ServletOutputStream getOutputStream() throws IOException
{
return null;
} public PrintWriter getWriter() throws IOException
{
// autoflush is true, println() will flush,
// but print() will not.
writer = new PrintWriter(output, true);
return writer;
} public boolean isCommitted()
{
return false;
} public void reset()
{
} public void resetBuffer()
{
} public void setBufferSize(int size)
{
} public void setContentLength(int length)
{
} public void setContentType(String type)
{
} public void setLocale(Locale locale)
{
} @Override
public String getContentType()
{
// TODO Auto-generated method stub
return null;
} @Override
public void setCharacterEncoding(String charset)
{
// TODO Auto-generated method stub }
}

ServletProcessor1.java

import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.io.File;
import java.io.IOException;
import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; public class ServletProcessor1 { public void process(Request request, Response response) { String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
URLClassLoader loader = null; try {
// create a URLClassLoader
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(Constants.WEB_ROOT);
// the forming of repository is taken from the createClassLoader method in
// org.apache.catalina.startup.ClassLoaderFactory
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
// the code for forming the URL is taken from the addRepository method in
// org.apache.catalina.loader.StandardClassLoader class.
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
}
catch (IOException e) {
System.out.println(e.toString() );
}
Class myClass = null;
try {
myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
System.out.println(e.toString());
} Servlet servlet = null; try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
} }
}

  

import java.io.IOException;

public class StaticResourceProcessor {

  public void process(Request request, Response response) {
try {
response.sendStaticResource();
}
catch (IOException e) {
e.printStackTrace();
}
}
}

  在静态资源的访问过程中,静态资源的的outpu过程放在了Response当中,其实是不合理的,应该放在Processor当中

---------------------------------------------------

private String parseUri(String requestString)
{
       int index1, index2;
       index1 = requestString.indexOf(' ');
       if (index1 != -1)
      {
            index2 = requestString.indexOf(' ', index1 + 1);
           if (index2 > index1)
                return requestString.substring(index1 + 1, index2);
       }
       return null;
}
Request类的parseURI的过程
利用了HTTP请求报文中的GET uri HTTP/1.1的规范找到第一行中间的uri字段

该资源是host后面的部分
比如www.baidu.com/guowuxin
则该字段便是/guowuxin
  

----------------------------------------------------------------------------------------------------------

下面讲一个这个程序的缺点
1:编程的OOP可能被破坏,封装性,传送给Servlet的ServletRequest对象本身应该
只能使用ServletRequest对象的相关方法,但是由于本身是子类Request对象,
所以如果认为进行强转,也是可以使用子类的方法的,破坏了封装性质,
不过这个问题可能出现在很多情景下,并不是一定要处理的额。

使用外观模式将这个类包起来就可以解决这个问题,
包接口的方法很类似于适配器模式,但是二者是不同的,
适配器模式是由于参数个数的不同或者方法名的不同而进行适配的,
这里是为了进一步的封装子类的方法。

package ex02.pyrmont;

import java.io.IOException;
import java.io.BufferedReader;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest; public class RequestFacade implements ServletRequest { private ServletRequest request = null; public RequestFacade(Request request) {
this.request = request;
} /* implementation of the ServletRequest*/
public Object getAttribute(String attribute) {
return request.getAttribute(attribute);
} public Enumeration getAttributeNames() {
return request.getAttributeNames();
} public String getRealPath(String path) {
return request.getRealPath(path);
} public RequestDispatcher getRequestDispatcher(String path) {
return request.getRequestDispatcher(path);
} public boolean isSecure() {
return request.isSecure();
} public String getCharacterEncoding() {
return request.getCharacterEncoding();
} public int getContentLength() {
return request.getContentLength();
} public String getContentType() {
return request.getContentType();
} public ServletInputStream getInputStream() throws IOException {
return request.getInputStream();
} public Locale getLocale() {
return request.getLocale();
} public Enumeration getLocales() {
return request.getLocales();
} public String getParameter(String name) {
return request.getParameter(name);
} public Map getParameterMap() {
return request.getParameterMap();
} public Enumeration getParameterNames() {
return request.getParameterNames();
} public String[] getParameterValues(String parameter) {
return request.getParameterValues(parameter);
} public String getProtocol() {
return request.getProtocol();
} public BufferedReader getReader() throws IOException {
return request.getReader();
} public String getRemoteAddr() {
return request.getRemoteAddr();
} public String getRemoteHost() {
return request.getRemoteHost();
} public String getScheme() {
return request.getScheme();
} public String getServerName() {
return request.getServerName();
} public int getServerPort() {
return request.getServerPort();
} public void removeAttribute(String attribute) {
request.removeAttribute(attribute);
} public void setAttribute(String key, Object value) {
request.setAttribute(key, value);
} public void setCharacterEncoding(String encoding)
throws UnsupportedEncodingException {
request.setCharacterEncoding(encoding);
} }
package ex02.pyrmont;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import javax.servlet.ServletResponse;
import javax.servlet.ServletOutputStream; public class ResponseFacade implements ServletResponse { private ServletResponse response;
public ResponseFacade(Response response) {
this.response = response;
} public void flushBuffer() throws IOException {
response.flushBuffer();
} public int getBufferSize() {
return response.getBufferSize();
} public String getCharacterEncoding() {
return response.getCharacterEncoding();
} public Locale getLocale() {
return response.getLocale();
} public ServletOutputStream getOutputStream() throws IOException {
return response.getOutputStream();
} public PrintWriter getWriter() throws IOException {
return response.getWriter();
} public boolean isCommitted() {
return response.isCommitted();
} public void reset() {
response.reset();
} public void resetBuffer() {
response.resetBuffer();
} public void setBufferSize(int size) {
response.setBufferSize(size);
} public void setContentLength(int length) {
response.setContentLength(length);
} public void setContentType(String type) {
response.setContentType(type);
} public void setLocale(Locale locale) {
response.setLocale(locale);
} }
package ex02.pyrmont;

import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException; public class HttpServer2 { // shutdown command
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; // the shutdown command received
private boolean shutdown = false; public static void main(String[] args) {
HttpServer2 server = new HttpServer2();
server.await();
} public void await() {
ServerSocket serverSocket = null;
int port = 8080;
try {
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
}
catch (IOException e) {
e.printStackTrace();
System.exit(1);
} // Loop waiting for a request
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream(); // create Request object and parse
Request request = new Request(input);
request.parse(); // create Response object
Response response = new Response(output);
response.setRequest(request); //check if this is a request for a servlet or a static resource
//a request for a servlet begins with "/servlet/"
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor2 processor = new ServletProcessor2();
processor.process(request, response);
}
else {
StaticResourceProcessor processor = new StaticResourceProcessor();
processor.process(request, response);
} // Close the socket
socket.close();
//check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}
catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
}

  

package ex02.pyrmont;

import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.io.File;
import java.io.IOException;
import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; public class ServletProcessor2 { public void process(Request request, Response response) { String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
URLClassLoader loader = null; try {
// create a URLClassLoader
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(Constants.WEB_ROOT);
// the forming of repository is taken from the createClassLoader method in
// org.apache.catalina.startup.ClassLoaderFactory
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
// the code for forming the URL is taken from the addRepository method in
// org.apache.catalina.loader.StandardClassLoader class.
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
}
catch (IOException e) {
System.out.println(e.toString() );
}
Class myClass = null;
try {
myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
System.out.println(e.toString());
} Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
} }
}

  

《深入剖析Tomcat》阅读(二)的更多相关文章

  1. 07.ElementUI 2.X 源码学习:源码剖析之工程化(二)

    0x.00 前言 项目工程化系列文章链接如下,推荐按照顺序阅读文章 . 1️⃣ 源码剖析之工程化(一):项目概览.package.json.npm script 2️⃣ 源码剖析之工程化(二):项目构 ...

  2. 深入剖析tomcat 笔记——第8章 载入器

    深入剖析tomcat 笔记 目录:

  3. WCF技术剖析之十二:数据契约(Data Contract)和数据契约序列化器(DataContractSerializer)

    原文:WCF技术剖析之十二:数据契约(Data Contract)和数据契约序列化器(DataContractSerializer) [爱心链接:拯救一个25岁身患急性白血病的女孩[内有苏州电视台经济 ...

  4. 深入剖析Tomcat会话机制

    1缓存机制 Tomcat默认将Session保存到内存中.但同时,Tomcat也提供了PersistentManager配合不同的Store实现的方式,使Session可以被保存到不同地方(Datab ...

  5. 深入剖析TOMCAT

    理解tomcat之搭建简易http服务器 做过java web的同学都对tomcat非常熟悉.我们在使用tomcat带来的便利的同时,是否想过tomcat是如何工作的呢?tomcat本质是一个http ...

  6. web服务器专题:tomcat(二)模块组件与server.xml 配置文件

    web服务器专题:tomcat(二)模块组件与server.xml 配置文件 回顾: Web服务器专题:tomcat(一) 基础模块 一个Server.xml的实例 <?xml version= ...

  7. 《深入剖析Tomcat》源码

    <深入剖析Tomcat>翻译自<How Tomcat Works> 可以到官网下载:https://brainysoftware.com/download 官网下载比较慢,我就 ...

  8. 《深入剖析Tomcat》读书笔记(二)

    三.容器Container Container 是容器的父接口,所有子容器都必须实现这个接口.Container 容器的设计用的是典型的责任链的设计模式,它有四个子容器组件构成,分别是:Engine. ...

  9. 《深入剖析Tomcat》阅读(三)

    这里要介绍下Tomcat的一个重要设计方法,Catalina设计方式. Servlet容器是一个复杂系统,但是,它有三个基本任务,对每个请求,servlet容器会为其完成以下三个操作: 1.创建一个R ...

随机推荐

  1. java.sql.SQLException: Before start of result set解决方法

    java.sql.SQLException: Before start of result set解决方法 今天做东西的时候发现这个错误,查了查,特地记下来,以后开始积累了 哈哈 解决发法是: 使用r ...

  2. Python中的判断、循环 if...else,while

    if...else语句: a=3; b=3; if a == b :print(a,b)elif a <= b :print(str(a) + " is less than " ...

  3. leetCode 70.Climbing Stairs (爬楼梯) 解题思路和方法

    Climbing Stairs  You are climbing a stair case. It takes n steps to reach to the top. Each time you ...

  4. Request的getParameter和getAttribute方法的区别

    下面整理一下getParameter和getAttribute的区别和各自的使用范围.      (1)HttpServletRequest类有setAttribute()方法,而没有setParam ...

  5. HDU1711-----Number Sequence-----裸的KMP

    题目地址:http://acm.hdu.edu.cn/showproblem.php?pid=1711 题目意思: 找出b在a中的起始位置,没有则是-1 解题思路: 裸的KMP,不多说 不会KMP的话 ...

  6. Android 获取运营商信息(完整版)-解决高通,MTK等双卡问题

    由于国内的运营商问题,双卡手机获取IMSI号问题要根据厂商API 来实现. 下面我们就来做一套完整的分析运营商获取IMSI号逻辑. 1,首先我们要判断手机的平台. 1.1,判断手机是否MTK平台 1 ...

  7. Chapter 5 - How to Detect the Collisions

    Chapter 5 - How to Detect the Collisions Our hero can fire bullets now, but the bullets are only vis ...

  8. Java基础知识强化之IO流笔记23:计算机是如何识别把两个字节拼接为中文(附加)

    1. 计算机是如何识别什么时候该把两个字节转换为一个中文呢? 在计算机中中文的存储分两个字节: • 第一个字节肯定是负数. • 第二个字节常见的是负数,可能有正数.但是没影响. 2. 代码示例: pa ...

  9. 【bzoj1212】 [HNOI2004]L语言

    题目描述 标点符号的出现晚于文字的出现,所以以前的语言都是没有标点的.现在你要处理的就是一段没有标点的文章. 一段文章T是由若干小写字母构成.一个单词W也是由若干小写字母构成.一个字典D是若干个单词的 ...

  10. gitcafe 使用hexo搭建博客

    --缘由:因为看大家都用github等搭建博客,作为半个程序员的自己,也按捺不住了,终于有空来尝试一把了,选择了和github 相同功能的gitcafe网站,因为在国内比较快,这是大家的看法,下面写一 ...