springmvc基础-视图与视图解析器详解

一、概述

  • 请求处理方法执行完成后,最终返回一个 ModelAndView 对象。对于那些返回 String,View 或 ModeMap 等类型的 处理方法,Spring MVC 也会在内部将它们装配成一个ModelAndView 对象,它包含了逻辑名和模型对象的视图。
  • Spring MVC 借助视图解析器(ViewResolver)得到最终 的视图对象(View),最终的视图可以是 JSP ,也可能是Excel、JFreeChart  等各种表现形式的视图。
  • 对于最终究竟采取何种视图对象对模型数据进行渲染,处 理器并不关心,处理器工作重点聚焦在生产模型数据的工 作上,从而实现 MVC 的充分解耦。

二、视图含义

  • 视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客 户。
  • 为了实现视图模型和具体实现技术的解耦,Spring 在org.springframework.web.servlet 包中定义了一个高度抽象的 View接口:

  • 视图对象由视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题

三、常用的视图实现类

四、视图解析器

  • SpringMVC 为逻辑视图名的解析提供了不同的策略,可 以在 Spring WEB 上下文中配置一种或多种解析策略,并 指定他们之间的先后顺序。每一种映射策略对应一个具体 的视图解析器实现类。
  • 视图解析器的作用比较单一:将逻辑视图解析为一个具体 的视图对象。
  • 所有的视图解析器都必须实现 ViewResolver 接口:

五、常用的视图解析器实现类

  • 程序员可以选择一种视图解析器或混用多种视图解析器
  • 每个视图解析器都实现了 Ordered 接口并开放出一个 order 属性,可 以通过 order 属性指定解析器的优先顺序,order 越小优先级越高。
  • SpringMVC 会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则将抛出 ServletException 异常

六、自定义视图

大白话总结:1、编辑正常的请求连接提交给控制器。2、控制器处理请求 返回视图名。3、视图解析器  利用视图名   从IOC容器中 获取 自定义视图。

比如 控制器返回”success”,

如果配置用InternalResourceViewResolver解析的视图,肯定有解析器的前缀和后缀,可能最终解析完路径是 /WEB-INF/views/success.jsp
[即实际地址是:网站根目录/WEB-INF/views/success.jsp]
因为如果不想用 解析器的前缀和后缀 ,那么控制器的返回值应该是 WEB-INF/views/success.jsp
补充:控制器返回字符串值 “/WEB-INF/views/success.jsp” 和这个效果一样 “WEB-INF/views/success.jsp”

如果配置用BeanNameViewResolver视图解析器,则会根据视图名去IOC容器里面,获取一个视图。

1、编辑测试页

在测试页面添加超链接

<body>
测试自定义view
<a href="testredirect">testdiyview</a>
</body>

2、编辑控制器

@RequestMapping("/diyview")
public String testdiyview(){
		
	System.out.println("test-diyview");
	//返回值是自定义视图的类名且第一个字母需小写
        return "helloView";
}

3、编辑转发配置页

<!-- 配置视图  BeanNameViewResolver 解析器: 使用视图的名字来解析视图 -->
<!-- 通过 order 属性来定义视图解析器的优先级, order 值越小优先级越高 , 至于InternalResourceViewResolver其优先级是最低的 -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
    <property name="order" value="100"></property>
</bean>

核心注意:当时在转发配置页中:扫描注解包时,扫描路径是
com.test.springmvc.handlers 所以com.test.springmvc.views 无法扫描到【views包下面有view类,里面有注解】所以正确做法是:

<!-- 对于注解的Handler可以单个配置 实际开发中建议使用组件扫描 -->
<!-- <bean class="com.test.springmvc.handlers.HelloWorld" />  -->
<!-- 可以扫描controller、service、...这里让扫描controller,指定controller的包,但是view的包下面的注解类就扫描不到了,
所以 com.test.springmvc.handlers 改成 com.test.springmvc就行啦,子目录都能扫描得到  -->
<!--下面是 配置扫描  指定目录包 中的 java文件 -->
<context:component-scan base-package="com.test.springmvc"></context:component-scan>

4、创建自定义view

package com.test.springmvc.views;

import java.util.Date;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.View;

//这个@Component注解的意思是  将该view 放到 IOC 容器里面 
@Component
public class HelloView implements View {

	@Override
	public String getContentType() {
		// TODO Auto-generated method stub
		return "text/html";
	}

	@Override
	public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		// TODO Auto-generated method stub
		response.getWriter().print("hello view ,time" + new Date());
	}

}

 

springmvc基础-控制器返回值总结

关于请求问题:
请求映射到了控制器,如果控制器处理后的返回值是 “success” ,经过解析器转换成 WEB-INF/views/success.jsp 【如果解析器没有前后缀,则控制器的返回值就必须是完整的,也就是带上了前后缀的】
最后的完整路径是 :网站根目录/WEB-INF/views/success.jsp
【当然,因为/WEB-INF 文件夹仅对服务器可见,对浏览器隐藏的,所以直接访问网站根目录/WEB-INF/views/success.jsp  是无法访问的】

springmvc的控制器【controller】或者叫Handler,它的返回值是:ModelAndView 类型。之前学习过说返回值可以是map或string。其实到最后,都会内部转化成ModelAndView 类型。

一、基础流程

测试: <a href=”springmvc/testModelAndView”>Test ModelAndView</a>

控制器:

/**
	 * 目标方法的返回值可以是 ModelAndView 类型。 
	 * 其中可以包含视图和模型信息
	 * SpringMVC 会把 ModelAndView 的 model 中数据放入到 request 域对象中. 
	 * @return
	 */
	@RequestMapping("/testModelAndView")
	public ModelAndView testModelAndView(){
		String viewName = SUCCESS;
		ModelAndView modelAndView = new ModelAndView(viewName);
		
		//添加模型数据到 ModelAndView 中.
		modelAndView.addObject("time", new Date());
		
		return modelAndView;
	}

控制器中 ModelAndView 的构造函数名,就是ModelAndView中的view里面的转发页面的名字。

返回页jsp编辑:
time: ${requestScope.time }

二、控制器中的map参数

控制器中的map参数,最后变成了返回值 ModelAndView 中的model。至于ModelAndView 中的view里面包含了返回页的名字。

测试: <a href=”springmvc/testMap”>Test Map</a>

控制器:

      /**
	 * 目标方法可以添加 Map 类型(实际上也可以是 Model 类型或 ModelMap 类型)的参数. 【因为是继承关系,最后都是一样的】
	 * @param map
	 * @return
	 */
	@RequestMapping("/testMap")
	public String testMap(Map<String, Object> map){
		System.out.println(map.getClass().getName()); 
		map.put("names", Arrays.asList("Tom", "Jerry", "Mike"));
		return "success";
	}

返回页jsp编辑:

names: ${requestScope.names }

三、控制器中的@SessionAttributes

@SessionAttributes : 该注解只能放在类的上面. 而不能修饰放方法. 

测试:<a href=”springmvc/testSessionAttributes”>Test SessionAttributes</a>

控制器:

@SessionAttributes(value={"user"}, types={String.class})
@RequestMapping("/springmvc")
@Controller
public class SpringMVCTest {

      /**
	 * @SessionAttributes 除了可以通过属性名指定需要放到会话中的属性外(实际上使用的是 value 属性值),
	 * 还可以通过模型属性的对象类型指定哪些模型属性需要放到会话中(实际上使用的是 types 属性值)
	 * 
	 * 注意: 该注解只能放在类的上面. 而不能修饰放方法. 
	 */
	@RequestMapping("/testSessionAttributes")
	public String testSessionAttributes(Map<String, Object> map){
		User user = new User("Tom", "123456", "[email protected]", 15);
		map.put("user", user);
		map.put("school", "atguigu");
		return "success";
	}

}

这个实验表示:加了@SessionAttributes 注解后,map会根据相应的关键字或类型名,额外放置一份数据到session中。原本map放置到request中的信息,仍会保留在request中。

返回值jsp页:

	request user: ${requestScope.user }
	<br><br>
	
	session user: ${sessionScope.user }
	<br><br>
	
	request school: ${requestScope.school }
	<br><br>
	
	session school: ${sessionScope.school }

四、控制器中的@ModelAttribute

@ModelAttribute 注解的方法,本质来说:会在其他方法执行前,会首先被调用。用于初始化一些参数。然后其他方法执行时,会先查找这些参数是否已经存在,若不存则创建一个。

这个例子:介绍的是从数据库中读取一段信息,然后修改了一部分,又重新保存到数据库中。
测试页:

<!--  
		模拟修改操作
		1. 原始数据为: 1, Tom, 123456,[email protected],12
		2. 密码不能被修改.
		3. 表单回显, 模拟操作直接在表单填写对应的属性值
	-->
	<form action="springmvc/testModelAttribute" method="Post">
		<input type="hidden" name="id" value="1"/>
		username: <input type="text" name="username" value="Tom"/>
		<br>
		email: <input type="text" name="email" value="[email protected]"/>
		<br>
		age: <input type="text" name="age" value="12"/>
		<br>
		<input type="submit" value="Submit"/>
	</form>

控制器:

       /**
	 * 1. 有 @ModelAttribute 标记的方法, 会在每个目标方法执行之前被 SpringMVC 调用! 
	 * 2. @ModelAttribute 注解也可以来修饰目标方法 POJO 类型的入参, 其 value 属性值有如下的作用:
	 * 1). SpringMVC 会使用 value 属性值在 implicitModel 中查找对应的对象, 若存在则会直接传入到目标方法的入参中.
	 * 2). SpringMVC 会一 value 为 key, POJO 类型的对象为 value, 存入到 request 中. 
	 */
	@ModelAttribute
	public void getUser(@RequestParam(value="id",required=false) Integer id, 
			Map<String, Object> map){
		System.out.println("modelAttribute method");
		if(id != null){
			//模拟从数据库中获取对象
			User user = new User(1, "Tom", "123456", "[email protected]", 12);
			System.out.println("从数据库中获取一个对象: " + user);
			
			map.put("user", user);
		}
	}
	
	/**
	 * 运行流程:
	 * 1. 执行 @ModelAttribute 注解修饰的方法: 从数据库中取出对象, 把对象放入到了 Map 中. 键为: user
	 * 2. SpringMVC 从 Map 中取出 User 对象, 并把表单的请求参数赋给该 User 对象的对应属性.
	 * 3. SpringMVC 把上述对象传入目标方法的参数. 
	 * 
	 * 注意: 在 @ModelAttribute 修饰的方法中, 放入到 Map 时的键需要和目标方法入参类型的第一个字母小写的字符串一致!
	 * 
	 * SpringMVC 确定目标方法 POJO 类型入参的过程
	 * 1. 确定一个 key:
	 * 1). 若目标方法的 POJO 类型的参数木有使用 @ModelAttribute 作为修饰, 则 key 为 POJO 类名第一个字母的小写
	 * 2). 若使用了  @ModelAttribute 来修饰, 则 key 为 @ModelAttribute 注解的 value 属性值. 
	 * 2. 在 implicitModel 中查找 key 对应的对象, 若存在, 则作为入参传入
	 * 1). 若在 @ModelAttribute 标记的方法中在 Map 中保存过, 且 key 和 1 确定的 key 一致, 则会获取到. 
	 * 3. 若 implicitModel 中不存在 key 对应的对象, 则检查当前的 Handler 是否使用 @SessionAttributes 注解修饰, 
	 * 若使用了该注解, 且 @SessionAttributes 注解的 value 属性值中包含了 key, 则会从 HttpSession 中来获取 key 所
	 * 对应的 value 值, 若存在则直接传入到目标方法的入参中. 若不存在则将抛出异常. 
	 * 4. 若 Handler 没有标识 @SessionAttributes 注解或 @SessionAttributes 注解的 value 值中不包含 key, 则
	 * 会通过反射来创建 POJO 类型的参数, 传入为目标方法的参数
	 * 5. SpringMVC 会把 key 和 POJO 类型的对象保存到 implicitModel 中, 进而会保存到 request 中. 
	 * 
	 * 源代码分析的流程
	 * 1. 调用 @ModelAttribute 注解修饰的方法. 实际上把 @ModelAttribute 方法中 Map 中的数据放在了 implicitModel 中.
	 * 2. 解析请求处理器的目标参数, 实际上该目标参数来自于 WebDataBinder 对象的 target 属性
	 * 1). 创建 WebDataBinder 对象:
	 * ①. 确定 objectName 属性: 若传入的 attrName 属性值为 "", 则 objectName 为类名第一个字母小写. 
	 * *注意: attrName. 若目标方法的 POJO 属性使用了 @ModelAttribute 来修饰, 则 attrName 值即为 @ModelAttribute 
	 * 的 value 属性值 
	 * 
	 * ②. 确定 target 属性:
	 * 	> 在 implicitModel 中查找 attrName 对应的属性值. 若存在, ok
	 * 	> *若不存在: 则验证当前 Handler 是否使用了 @SessionAttributes 进行修饰, 若使用了, 则尝试从 Session 中
	 * 获取 attrName 所对应的属性值. 若 session 中没有对应的属性值, 则抛出了异常. 
	 * 	> 若 Handler 没有使用 @SessionAttributes 进行修饰, 或 @SessionAttributes 中没有使用 value 值指定的 key
	 * 和 attrName 相匹配, 则通过反射创建了 POJO 对象
	 * 
	 * 2). SpringMVC 把表单的请求参数赋给了 WebDataBinder 的 target 对应的属性. 
	 * 3). *SpringMVC 会把 WebDataBinder 的 attrName 和 target 给到 implicitModel. 
	 * 近而传到 request 域对象中. 
	 * 4). 把 WebDataBinder 的 target 作为参数传递给目标方法的入参. 
	 */
	@RequestMapping("/testModelAttribute")
	public String testModelAttribute(User user){
		System.out.println("修改: " + user);
		return "success";
	}

转发显示jsp页:

request user: ${requestScope.user }

五、视图和视图解析器

请求处理方法执行完成后,最终返回一个 ModelAndView 对象。对于那些返回 String,View 或 ModeMap 等类型的处理方法,Spring MVC 也会在内部将它们装配成一个ModelAndView 对象,它包含了逻辑名和模型对象的视图。

Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是 JSP ,也可能是Excel、JFreeChart 等各种表现形式的视图。

对于最终究竟采取何种视图对象对模型数据进行渲染,处理器并不关心,处理器工作重点聚焦在生产模型数据的工作上,从而实现 MVC 的充分解耦。

六、重定向和转发

  • 一般情况下,控制器方法返回字符串类型的值会被当成逻 辑视图名处理
  • 如果返回的字符串中带 forward:redirect: 前缀时,SpringMVC 会对他们进行特殊处理:将 forward: 和redirect: 当成指示符,其后的字符串作为 URL 来处理

–redirect:success:请求重定向到  ->  站点入口所在目录/success ,继续交由handler处理

–forward:success:请求转发到    ->  站点入口所在目录/success ,继续交由handler处理【不过转发时,请求路径是不会跳转的】

测试页:

@RequestMapping("/testredirect")
public String testRedirect(){

System.out.println("test-redirect");

//return "redirect:/index";
return "forward:/success";

}

七、静态资源请求

核心说明:
1、如果url请求对象是 xxx.jsp ,就按照网站路径去查找对应的文件。
2、如果url请求对象不是 xxx.jsp 比如 xx.html 或者 xx ,就交给视图解析器【如果设置了前缀和后缀,就转换成最终路径】去查询路径中是否存在文件。

这样,就造成了无法直接请求静态资源的问题:(在配置开发环境中已提到)

为了能够在网页中请求静态资源,如加载js和css等,所以需要  编辑   转发配置器。

 <!--  
		default-servlet-handler 将在 SpringMVC 上下文中定义一个 DefaultServletHttpRequestHandler,
		它会对进入 DispatcherServlet 的请求进行筛查, 如果发现是没有经过映射的请求, 就将该请求交由 WEB 应用服务器默认的 
		Servlet 处理. 如果不是静态资源的请求,才由 DispatcherServlet 继续处理

		一般 WEB 应用服务器默认的 Servlet 的名称都是 default.
		若所使用的 WEB 服务器的默认 Servlet 名称不是 default,则需要通过 default-servlet-name 属性显式指定
		
	-->
	<mvc:default-servlet-handler/>
	<!-- 前面的 mvc:default-servlet-handler 可以解决静态资源了,但是让其他经过handler的请求,又失效了,所以配置了下面的 mvc:annotation-driven 来解决问题 -->
	<mvc:annotation-driven></mvc:annotation-driven>

 

 

 

springmvc基础-请求映射总结

@RequestMapping  请求映射总结

注意:

@RequestMapping(“/helloworld”)
public String hello( HttpServletRequest request ,HttpServletResponse response){
System.out.println(“hello request” + request.toString());
System.out.println(“hello response” + response.toString());
return “success”;
}
handler 控制器:函数参数 可以有request和 response,可以有其中一个,也可以都没有,request和response 都是spring容器注入的。他们是接口。还有将request 和 response 内部对象  作为形参的 。另外也可以将  map【返回值ModelAndView中的model】作为形参。

经过了测试:如果  控制器的映射是这个–>@RequestMapping("/") 那么访问网站根目录时,还是以 网站根目录下的 index.jsp、 index.html  等做为结果 返回给浏览器。


一、请求映射-路径区分

1、请求映射-路径区分-基础

<a href="springmvc/testRequestMapping">Test RequestMapping</a>
package com.test.springmvc.handlers;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller // spring里面 又名handler 处理器
public class HelloWorld {

	/**
	 * 1、使用RequestMapping 注解来映射 请求的url
	 * 2、返回值会通过视图解析器解析为实际的物理视图,
	 * 对于org.springframework.web.servlet.view.InternalResourceViewResolver
	 * 会做如下解析,prefix + returnVal + suffix 得到实际的物理视图。然后做转发操作。
	 * 即 WEB-INF/views/success.jsp
 	 */
	@RequestMapping("/helloworld")
	public String hello( HttpServletRequest request,HttpServletResponse response){
		System.out.println("hello request" + request.toString());
		System.out.println("hello response" + response.toString());
		return "success";
	}
	
}

2、请求映射-路径区分-利用post方式

<form action="springmvc/testMethod" method="POST">
	<input type="submit" value="submit"/>
</form>
/**
	 * 常用: 使用 method 属性来指定请求方式
	 */
	@RequestMapping(value = "/testMethod", method = RequestMethod.POST)
	public String testMethod() {
		System.out.println("testMethod");
		return "success";
	}

3、请求映射-路径区分-利用请求参数和请求头

	<a href="springmvc/testParamsAndHeaders?username=atguigu&age=10">Test ParamsAndHeaders</a>
/**
	 * 了解: 可以使用 params 和 headers 来更加精确的映射请求. params 和 headers 支持简单的表达式.
	 * 
	 * @return
	 */
	@RequestMapping(value = "testParamsAndHeaders", params = { "username","age!=10" }, headers = { "Accept-Language=en-US,zh;q=0.8" })
	public String testParamsAndHeaders() {
		System.out.println("testParamsAndHeaders");
		return "success";
	}

4、请求映射-路径区分-利用ant风格

Ant 风格资源地址支持3种匹配符:
?:匹配文件名中的一个字符
*:匹配一层路径
**:** 匹配多层路径

测试: <a href=”springmvc/testAntPath/mnxyz/abc”>Test AntPath</a>

@RequestMapping("/testAntPath/*/abc")
	public String testAntPath() {
		System.out.println("testAntPath");
		return "success";
	}

二、请求映射-信息传递

1、请求映射-信息传递-利用路径变量

测试: <a href=”springmvc/testPathVariable/1″>Test PathVariable</a>

/**
	 * @PathVariable 可以来映射 URL 中的占位符到目标方法的参数中.
	 * @param id
	 * @return
	 */
	@RequestMapping("/testPathVariable/{id}")
	public String testPathVariable(@PathVariable("id") Integer id) {
		System.out.println("testPathVariable: " + id);
		return "success";
	}

2、请求映射-信息传递-REST风格

示例:
/order/1 HTTP  GET :得到id = 1的 order –
/order/1 HTTP DELETE:删除 id = 1 的 order –
/order/1 HTTP PUT:更新 id = 1的 order –
/order HTTP POST:新增 order –

HiddenHttpMethodFilter:浏览器 form 表单只支持 GET与POST请求,而DELETE、PUT 等method并不支持,Spring3.0 添加了一个过滤器,可以将这些请求转换为标准的 http 方法,使得支持 GET、POST、PUT 与DELETE 请求。

第一步:首先在web.xml的添加过滤器

<!-- 
	配置 org.springframework.web.filter.HiddenHttpMethodFilter: 可以把 POST 请求转为 DELETE 或 POST 请求 
	-->
	<filter>
		<filter-name>HiddenHttpMethodFilter</filter-name>
		<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
	</filter>
	
	<filter-mapping>
		<filter-name>HiddenHttpMethodFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

第二步:编写测试网页

<form action="springmvc/testRest/1" method="post">
		<input type="hidden" name="_method" value="PUT"/>
		<input type="submit" value="TestRest PUT"/>
	</form>
	<br><br>
	
	<form action="springmvc/testRest/1" method="post">
		<input type="hidden" name="_method" value="DELETE"/>
		<input type="submit" value="TestRest DELETE"/>
	</form>
	<br><br>
	
	<form action="springmvc/testRest" method="post">
		<input type="submit" value="TestRest POST"/>
	</form>
	<br><br>
	
	<a href="springmvc/testRest/1">Test Rest Get</a>
	<br><br>

3、请求映射-信息传递-利用请求参数

	/**
	 * @RequestParam 来映射请求参数. value 值即请求参数的参数名 required 该参数是否必须. 默认为 true
	 *               defaultValue 请求参数的默认值
	 */
	@RequestMapping(value = "/testRequestParam")
	public String testRequestParam(
			@RequestParam(value = "username") String un,
			@RequestParam(value = "age", required = false, defaultValue = "0") int age) {
		System.out.println("testRequestParam, username: " + un + ", age: "
				+ age);
		return "success";
	}

测试: <a href=”springmvc/testRequestParam?username=atguigu&age=11″>Test RequestParam</a>

其实也可以用表单的post方法,来传递请求参数,因为控制器也能解析的。

第三步:编写控制器

/**
	 * Rest 风格的 URL. 以 CRUD 为例: 新增: /order POST 修改: /order/1 PUT update?id=1 获取:
	 * /order/1 GET get?id=1 删除: /order/1 DELETE delete?id=1
	 * 
	 * 如何发送 PUT 请求和 DELETE 请求呢 ? 1. 需要配置 HiddenHttpMethodFilter 2. 需要发送 POST 请求
	 * 3. 需要在发送 POST 请求时携带一个 name="_method" 的隐藏域, 值为 DELETE 或 PUT
	 * 
	 * 在 SpringMVC 的目标方法中如何得到 id 呢? 使用 @PathVariable 注解
	 * 
	 */
	@RequestMapping(value = "/testRest/{id}", method = RequestMethod.PUT)
	public String testRestPut(@PathVariable Integer id) {
		System.out.println("testRest Put: " + id);
		return "success";
	}

	@RequestMapping(value = "/testRest/{id}", method = RequestMethod.DELETE)
	public String testRestDelete(@PathVariable Integer id) {
		System.out.println("testRest Delete: " + id);
		return "success";
	}

	@RequestMapping(value = "/testRest", method = RequestMethod.POST)
	public String testRest() {
		System.out.println("testRest POST");
		return "success";
	}

	@RequestMapping(value = "/testRest/{id}", method = RequestMethod.GET)
	public String testRest(@PathVariable Integer id) {
		System.out.println("testRest GET: " + id);
		return "success";
	}

4、请求映射-信息传递-利用请求头

测试: <a href=”springmvc/testRequestHeader”>Test RequestHeader</a>

/**
	 * 了解: 映射请求头信息 用法同 @RequestParam
	 */
	@RequestMapping("/testRequestHeader")
	public String testRequestHeader(
			@RequestHeader(value = "Accept-Language") String al) {
		System.out.println("testRequestHeader, Accept-Language: " + al);
		return "success";
	}

5、请求映射-信息传递-利用cookie值

测试:<a href=”springmvc/testCookieValue”>Test CookieValue</a>

/**
	 * 了解:
	 * 
	 * @CookieValue: 映射一个 Cookie 值. 属性同 @RequestParam
	 */
	@RequestMapping("/testCookieValue")
	public String testCookieValue(@CookieValue("JSESSIONID") String sessionId) {
		System.out.println("testCookieValue: sessionId: " + sessionId);
		return "success";
	}

6、请求映射-信息传递-form表单的pojo

将  表单信息  转换成  bean  ,支持级联属性。如:dept.deptId、dept.address.tel 等

1、测试页:

<form action="springmvc/testPojo" method="post">
		username: <input type="text" name="username"/>
		<br>
		password: <input type="password" name="password"/>
		<br>
		email: <input type="text" name="email"/>
		<br>
		age: <input type="text" name="age"/>
		<br>
		city: <input type="text" name="address.city"/>
		<br>
		province: <input type="text" name="address.province"/>
		<br>
		<input type="submit" value="Submit"/>
	</form>

2、控制页

	/**
	 * Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配, 自动为该对象填充属性值。支持级联属性。
	 * 如:dept.deptId、dept.address.tel 等
	 */
	@RequestMapping("/testPojo")
	public String testPojo(User user) {
		System.out.println("testPojo: " + user);
		return "success";
	}

3、Bean [估计是调用默认构造函数,然后set属性,完成bean创建]

bean – user

public class User {
	
	private Integer id;
	
	private String username;
	private String password;

	private String email;
	private int age;
	
	private Address address;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public Address getAddress() {
		return address;
	}

	public void setAddress(Address address) {
		this.address = address;
	}

	public User() {}
}

bean-address

public class Address {

	private String province;
	private String city;

	public String getProvince() {
		return province;
	}

	public void setProvince(String province) {
		this.province = province;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}
}

 

显示jsp页:

<%@ taglib uri=“http://java.sun.com/jsp/jstl/core” prefix=“c”%>

request user: ${requestScope.user }

核心摘要:在控制器中,自动生成的pojo的 User类型对象。会自动产生一个首字母小写的类型对象的关键字,并将关键字和其类型对象值,一同传入request中。

如这里:会将 “user” 作为关键字,user 对象作为value值,存放到request中。 

操作和 request.setAttribute("user", user);一样
如果控制器的pojo参数用了 @ModelAttribute 注解标记,那么标记值若是XXX,则效果就像 request.setAttribute("XXX", user);一样

//举例如下:
//控制器中 用 @ModelAttribute&nbsp;注解标记 的情况
public String testPojo(@ModelAttribute(value ="test") User user) {
	System.out.println("testPojo: " + user);
	return "success";
}
//在转发显示页 用 ${requestScope.test} 调用。当然别忘记添加 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

总之一旦有pojo对象生成,那显示jsp页就可以直接调用${requestScope.标记名 }


7、请求映射-信息传递-原始servletapi作为参数

测试:<a href=”springmvc/testServletAPI”>Test ServletAPI</a>

/**
	 * 可以使用 Serlvet 原生的 API 作为目标方法的参数 具体支持以下类型
	 * 
	 * HttpServletRequest 
	 * HttpServletResponse 
	 * HttpSession
	 * java.security.Principal 
	 * Locale InputStream 
	 * OutputStream 
	 * Reader 
	 * Writer
	 * @throws IOException 
	 */
	@RequestMapping("/testServletAPI")
	public void testServletAPI(HttpServletRequest request,
			HttpServletResponse response, Writer out) throws IOException {
		System.out.println("testServletAPI, " + request + ", " + response);
		out.write("hello springmvc");
//		return "success";
	}

Handler的函数参数可以添加上面的这些,其实本质上来说:函数参数只有 request和response。其他的参数都是这些方法里面包括的。比如writer就是response.getWriter()


springmvc踩坑-配置视图解析前后缀内容重复

配置文件里面的 配置内容重复了。

当时在springmvc中,配置转发器时,发现视图解析器 居然有两个。一个添加了前缀和后缀,一个没有添加。

然后我在handler【也叫controller】中,    —》{这个是非注解配置的handler}

@Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception{ 
    
    //新建ModelAndView
    ModelAndView modelAndView =  new ModelAndView();
    //相当 于request的setAttribut,在jsp页面中通过itemsList取数据
    modelAndView.addObject("itemsList", ""); 
   
 
    //指定视图 (本来这里就是 最终返回地址了,但是视图解析器有前缀和后缀 干扰了;正确做是删除视图解析器的前缀和后缀)
    modelAndView.setViewName("/WEB-INF/views/success.jsp");
    
    //或者 正确的做法 这里返回success;在视图解析器添加 前缀和后缀
    //modelAndView.setViewName("success");

    return modelAndView;

}

发现,返回的路径一直是:/WEB-INF/views/WEB-INF/views/success.jsp.jsp
所以一直报错了。。。

服务器开发-概述

想做一个简单的服务器,用来更好的了解web交互模式。


http1.0 简单服务器 :三个文件

package com.httpserver;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

import javax.swing.JTextArea;
import javax.swing.JTextField;


public class HttpResponse extends Thread {
	Socket socket;
	JTextArea ta;
	PrintStream pout;
	boolean isHttp1 = false;
	String path = null;
	HttpResponse(Socket socket, JTextArea ta,JTextField path)
	{
		this.socket = socket;
		this.ta = ta;
		this.path = path.getText();
	}
	
	public void run()
	{
		try {
		
			BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			pout = new PrintStream(socket.getOutputStream());
			String requestLine = br.readLine();
			//String requestCmds = ""+requestLine;
			if(requestLine==null){
				error(400,"Empty Request");
			}
			else{
				ta.append("Http请求,来自:["+socket.getInetAddress()+":"+socket.getPort()+"]   "+requestLine+"\n");
			}
			if(requestLine.toLowerCase().indexOf("http/1.")!=-1){
				isHttp1 = true;
			}
			String[] request = requestLine.split(" ");
			if(request.length<2)
			{
				error(400,"Bad Request");
			}
			String str1 = request[0];
			if(str1.equals("GET")){
				serveFile(request[1]);
			}
			else{
				error(400, "Bad Request");
				ta.append("Bad Request");
			}
			
			socket.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			//e.printStackTrace();
			return;
		}
		
	}
	
	
	private void error(int erorcd, String erortx) {
		erortx = "<html><h1>" + erortx + "</h1></html>";
		if (isHttp1) {
			pout.println("HTTP/1.0 " + erorcd + " " + erortx);
			pout.println("Content-type: text/html");
			pout.println("Content-length: " + erortx.length() + "\n");
		}
		pout.println(erortx);
	}
	
	private void serveFile(String requestPath)  {
		if (requestPath.equals("/")){
			/**
			 * 取首页文件,首页文件可以为index.html或index.htm
			 */
			requestPath = "/index.html";
			if(path==null){
				path=new File("").getAbsolutePath();
			}
			if(!new File(path+requestPath).exists()){
				requestPath="/index.htm";
			}			
		}				
		try {
			sendFileData(requestPath);
			ta.append("文件传输成功 !       "+requestPath+"\n");
		} catch (Exception e) {
			error(404, "");
			ta.append("请求文件不存在\n");
		}		
	}
	
	private void sendFileData(String requestPath) throws IOException,FileNotFoundException {
		InputStream inputstream = new FileInputStream(path+requestPath);
		if (inputstream == null)
			throw new FileNotFoundException(requestPath);		
		if (isHttp1) {
			pout.println("HTTP/1.0 200 Document follows");
			pout.println("Content-length: " + inputstream.available());
			
			if (requestPath.endsWith(".gif"))
				pout.println("Content-type: image/gif");
			else if (requestPath.endsWith(".jpg"))
				pout.println("Content-type: image/jpeg");
			else if (requestPath.endsWith(".png") || requestPath.endsWith(".htm"))
				pout.println("Content-Type: text/png");
			
			else if (requestPath.endsWith(".css") || requestPath.endsWith(".htm"))
				pout.println("Content-Type: text/css");
			else if (requestPath.endsWith(".js") || requestPath.endsWith(".htm"))
				pout.println("Content-Type: text/javascript");

			else if (requestPath.endsWith(".html") || requestPath.endsWith(".htm"))
				pout.println("Content-Type: text/html");

			else
				pout.println("Content-Type: application/octet-stream");
			
			pout.println("Connection: Keep-Alive");//println 表示写完自带换行
			/*****下面这个换行回车,必须要有 这个是报头 格式******/
			pout.println();
			/***** 但是这个 只需要一个换行回车就行了 **/
			//pout.println();

           
			
		}
		/*缓冲区设为8K*/
		byte[] is = new byte[8*1024];
		/*实际测试 缓冲区 太小了,请求一张 大于 8K 的图片就 无法传输了,好像不是这个问题*/
		//byte[] is = new byte[300*1024];

		
		int length=0;
		while((length=inputstream.read(is))!=-1){
			pout.write(is, 0, length);
		}
		inputstream.close();
		pout.flush();
	}

}
package com.httpserver;

import java.net.*;

import javax.swing.*;

public final class ServerThread extends Thread {
	int port;
	JTextArea display;
	JTextField status;
	JTextField direc;
	//JTextField ipAdd;
	ServerSocket listener = null;
	ServerThread (int port, JTextArea display, JTextField status,JTextField direc,JTextField ipAdd) throws Exception
	{
		this.port = port;
		this.display = display;
		this.status = status;
		this.direc = direc;
		byte[] ip = new byte[4];
		String ipp = ipAdd.getText();
		//System.out.println(ipp);
		String[] ip1 = ipp.split("\\.");
		for(int i = 0;i<ip1.length;i++){
			try{
				ip[i] = (byte) Integer.parseInt(ip1[i]);
			}
			catch(Exception e){
				ip[i] = -1;
			}
			//System.out.println(ip1[i]);
		}
		//System.out.println(ip[0]+" "+ip[1]+" "+ip[2]+" "+ip[3]);
		//this.ipAdd = ipAdd;
		try{
			listener = new ServerSocket(port,20, InetAddress.getByAddress(ip));
		}
		catch(Exception e){
			
			listener = new ServerSocket(port,20);
			display.append("无效的IP地址,已使用默认地址:"+InetAddress.getLocalHost().getHostAddress()+"\n");
			ipAdd.setText(InetAddress.getLocalHost().getHostAddress());
		}
		display.append("服务器已开启,端口:"+port+"\n");
		status.setText("服务器已开启。 "+ipAdd.getText()+":"+port+"  "+direc.getText());
	}
	
	public void run()
	{
		
		
		while(true)
		{
			try{
				Socket socket = listener.accept();
				/*for(String s = new BufferedReader(new InputStreamReader(socket.getInputStream())).readLine();s!="\n";
						s = new BufferedReader(new InputStreamReader(socket.getInputStream())).readLine()){
					display.append(new BufferedReader(new InputStreamReader(socket.getInputStream())).readLine());
				}
				*/
				/*BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				for(String s = br.readLine();s!=null;s = br.readLine()){
					display.append(s+"\n");
				}*/
				new HttpResponse(socket, display, direc).start();
			}
			catch(Exception e){
				display.append(e+"\n");
			}
		}
	}
	
}
package com.httpserver;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.plaf.synth.SynthStyle;

public class Surface implements ActionListener {

	JFrame jf = new JFrame("Web Server");
	JLabel iP = new JLabel("IP: ");
	JLabel port = new JLabel("Port: ");
	JTextField ipAdd = new JTextField(15);
	JTextField portNum = new JTextField(4);
	JButton start = new JButton("Start");
	JButton over = new JButton("Stop");
	JLabel direct = new JLabel("Main Directory: ");
	JTextField directory = new JTextField(24);
	JButton br = new JButton("...");
	// JLabel diary = new JLabel("Diary:");
	JTextArea ta = new JTextArea(10, 40);
	// JTextField outDirct = new JTextField(15);
	// JTextField outIp = new JTextField(8);
	JTextField outStatus = new JTextField(40);
	// JLabel status = new JLabel("Status:");
	ServerThread listener = null;
	boolean hasStarted = false;
	String ipp;

	public void init() {
		JPanel jp1 = new JPanel();
		jp1.add(iP);
		jp1.add(ipAdd);
		jp1.add(port);
		jp1.add(portNum);
		jp1.add(start);
		jp1.add(over);
		JPanel jp2 = new JPanel();
		jp2.add(direct);
		jp2.add(directory);
		jp2.add(br);
		// JPanel jp3 = new JPanel();
		// jp3.add(diary);
		// JScrollPane jsp = new JScrollPane();
		// jsp.add(ta);
		Border bb = BorderFactory.createEtchedBorder();
		Border tb1 = BorderFactory.createTitledBorder(bb, "Console");
		Border tb2 = BorderFactory.createTitledBorder(bb, "Diary");
		JScrollPane jsp = new JScrollPane(ta);
		JPanel jp4 = new JPanel();
		jp4.add(jsp);
		jp4.setBorder(tb2);
		ta.setLineWrap(true);
		ta.setEditable(false);
		start.addActionListener(this);
		over.addActionListener(this);
		br.addActionListener(this);
		Box jb1 = Box.createVerticalBox();
		jb1.add(jp1);
		jb1.add(jp2);
		jb1.setBorder(tb1);
		directory.setText("/Users/hello/Sites/testwebsites");
		// Box jb2 = Box.createVerticalBox();
		// jb2.add(jp3);
		// jb2.add(jp4);
		Box jb = Box.createVerticalBox();
		// jb.setBorder(bb);
		outStatus.setBackground(jf.getBackground());
		outStatus.setBorder(null);
		outStatus.setEditable(false);
		JPanel jp5 = new JPanel();
		jp5.setBorder(bb);
		// jp5.add(status);
		jp5.add(outStatus);
		outStatus.setText("服务器已停止");
		try {
			ipAdd.setText(InetAddress.getLocalHost().getHostAddress());
		} catch (Exception e) {
			ipAdd.setText("0.0.0.0");
		}
		jb.add(jb1);
		jb.add(jp4);
		jb.add(jp5);
		// jb.add(jp3);
		// jb.add(jp4);
		jf.add(jb);
		// jf.add(jp1);
		jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		jf.setResizable(false);
		jf.pack();
		jf.setVisible(true);

	}

	public void startServer() {
		start.setEnabled(false);
		over.setEnabled(true);
		int port = 8080;
		try {
			port = Integer.parseInt(portNum.getText());
		} catch (Exception e) {
			ta.append("端口号异常,已使用8080端口。\n");
			portNum.setText("8080");
		}
		if (hasStarted && port == listener.port) {
			listener.resume();
			ta.append("服务器已开启,端口:" + listener.port + "\n");
			outStatus.setText("服务器已开启。" + ipp + ":" + port + "  " + directory.getText());
			// ipAdd.setText(InetAddress.getLocalHost().getHostAddress());
			return;
		}

		try {
			listener = new ServerThread(port, ta, outStatus, directory, ipAdd);
			ipp = ipAdd.getText();
		} catch (Exception e) {
			ta.append("端口已被其他程序占用,请重试。\n");
			listener = null;
			hasStarted = false;
		}
		if (listener == null) {
			start.setEnabled(true);
		} else {
			hasStarted = true;
			listener.start();
		}
	}

	public void exitServer() {
		ta.append("服务器关闭。\n");
		if (listener != null) {
			listener.stop();
		}
		System.exit(0);
	}

	public void stopServer() {
		start.setEnabled(true);
		over.setEnabled(false);
		listener.suspend();
		ta.append("服务器已停止。\n");
		outStatus.setText("服务器已停止。");
	}

	public void selectPath() {
		String str = "";
		String loc = "";
		JFileChooser chooser = new JFileChooser();
		chooser.setCurrentDirectory(new java.io.File("."));
		chooser.setDialogTitle("主目录");
		chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		chooser.setAcceptAllFileFilterUsed(false);
		if (chooser.showOpenDialog(jf) == JFileChooser.APPROVE_OPTION) {
			// str += chooser.getCurrentDirectory();
			str += chooser.getSelectedFile();
			loc = str.replaceAll("\\\\", "/"); //// Windows路径到JAVA路径的转换
		}
		directory.setText(loc);
	}

	public void actionPerformed(ActionEvent event) {
		String command = event.getActionCommand();
		if (command.equals("Start")) {
			startServer();
		}
		if (command.equals("Stop")) {
			stopServer();
		}
		if (command.equals("Exit")) {
			exitServer();
		}
		if (command.equals("...")) {
			selectPath();
		}
	}

	public static void main(String[] args) {
		Surface sf = new Surface();
		sf.init();
		// sf.outStatus.setText("服务器已开启:220.111.332.444:8000 sdafsagassasa");

		 String a = new String("源码下载站");
		 System.out.println("16制"+strTo16(a));

		char c = '8';
		System.out.printf("The value of char %c  %X,is %d.%n", c, (int)c,(int)c);
        //16进制 转成10进制。  java 内部编码是 utf-16 
		
		String str = String.valueOf(c);
		byte[] bys;
		try {
			bys = a.getBytes("UTF-8");
			for (int i = 0; i < bys.length; i++) {
				System.out.printf("%X ", bys[i]);
			}
            
			//因为是Unicode编码,所以编码前有字节序。
			//(byte & 0xFF) 这是一个字节相与,然后 << 8  代表数据 左移 8位 ,相当于 两字节大小 与后面的 或运算
			// 相当于 地位的字节 复制了 新添加的 字节
			//int unicode = (bys[2] & 0xFF) << 8 | (bys[3]& 0xFF);
			//System.out.printf("The unicode value of %c is %d.%n", c, unicode);

		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	public static String strTo16(String s) {
		String str = "";

		System.out.println("字符串长度"+s.length());
		for (int i = 0; i < s.length(); i++) {
			int ch = (int) s.charAt(i);
			String s4 = Integer.toHexString(ch);
			str = str + s4;
		}
		return str;
	}

}

参考自:GitHub

另,Tomcat源码剖析电子书:

代码和UML图:https://github.com/Aresyi/HowTomcatWorks 
排版更好的百度电子书:https://yuedu.baidu.com/ebook/ac92f0d35122aaea998fcc22bcd126fff7055d60

网络编程资料:

自定义 服务器 css 设置成了 二进制类型 导致加载 不出来 样式,报文头和体 有且只有空一行 才能 被 浏览器 正确 解析。空两行 浏览器 加载不了 图片

http://localhost/~cool/beike/index.html

http://www.ietf.org/rfc/rfc1945.txt

 

http编码

https://blog.csdn.net/hongxingxiaonan/article/details/49963643

tcp连接详解:

https://blog.csdn.net/liuxinmingcode/article/details/50376035

tcp包长度

https://blog.csdn.net/lishanmin11/article/details/77045745

http 长度

https://blog.csdn.net/zerooffdate/article/details/78962818

http详解

https://segmentfault.com/a/1190000006689767#articleHeader8

https://blog.csdn.net/u012813201/article/details/70211255

websocket

https://www.jianshu.com/p/f666da1b1835

http://www.cnblogs.com/hustskyking/p/websocket-with-node.html

https://www.cnblogs.com/oshyn/p/3574497.html

浏览器开发-概述

想做个很简单浏览器,这样做可以,更加方便的web交互基础。不过很麻烦。这里留个档案吧。


浏览器内涵

 所谓的“浏览器内核”无非指的是一个浏览器最核心的部分——“Rendering Engine”,直译这个词汇叫做“渲染引擎”,不过我们也常称其为“排版引擎”、“解释引擎”。这个引擎的作用是帮助浏览器来渲染网页的内容,将页面内容和排版代码转换为用户所见的视图。

注:有时候我们所说的“浏览器内核”甚至“渲染引擎”,其实除了渲染引擎,也悄悄包含了javascript引擎,如WebKit,它由渲染引擎WebCore和javascript引擎JSCore组成。

常见的浏览器内核(或者说渲染引擎)有很多个,如Trident、Gecko、WebKit等等,不同的内核对网页编写语法的解释也有不同,进而导致同一个页面在不同内核的浏览器下显示出来的效果也会有所出入,这也是前端工程师需要让作品兼容各种浏览器的原因。

我们常常喜欢把浏览器内核与某浏览器名称直接挂钩起来,如IE内核、Chrome内核,其实是不全面的说法。比如Opera在7.0版本到12.16版本中采用的是独立研发的Presto引擎,但在后续跟随了Chrome的脚步加入了WebKit大本营,放弃了Presto;另外即使名称相同,但版本不同的引擎也可能存在较大差别。比如IE6使用的是Trident早期版本,存在许多bug,性能也较低。而最新的IE11所使用的Trident7.0版本已经可以支持WebGL(3D绘图标准)以及HTML5大部分标准。

下面按照各个主流浏览器,介绍下它们所使用的浏览器内核的历程。

Internet Explorer:

IE开发计划开始于1994年夏天,微软为抵抗当时主流的网景Netscape Navigator,要在Windows中开发适合自己的浏览器,但微软并没有时间从零开始。因此和Spyglass合作,于是IE从早期一款商业性的专利网页浏览器Spyglass Mosaic派生出来,虽然Spyglass Mosaic与NCSA Mosaic(首款应用得最广泛的网页浏览器)甚为相似,但Spyglass Mosaic则相对地较不出名并使用了NCSA Mosaic少量的源代码。

1996年,微软通过给予季度费用和部分收入从Spyglass中取得了Spyglass Mosaic的源代码和授权。从而使IE逐渐成为微软专属软件。它采用的排版引擎(俗称内核)为Trident。每一次新的IE版本发布,也标志着Trident内核版本号的提升。

下面是各Trident版本信息:

冷知识:除Trident之外,微软还有另一个网页浏览器排版引擎,称为Tasman,它是使用在「Internet Explorer for Mac」的排版引擎。相较于Trident,Tasman引擎对网页标准有较佳的支持,但微软自04年开始已经停止了Mac计算机版本的 Internet Explorer的开发。

Safari

        Safari是苹果公司开发的浏览器,使用了KDE(Linux桌面系统)的KHTML作为浏览器的运算核心,Safari所用浏览器内核的名称是大名鼎鼎的WebKit。 Safari在2003年1月7日首度发行测试版,并成为Mac OS X v10.3与之后版本的默认浏览器,也成为苹果其它系列产品的指定浏览器(也已支持Windows平台)。

        如上述可知,WebKit前身是KDE小组的KHTML引擎,可以说WebKit是KHTML的一个开源的分支。当年苹果在比较了Gecko和KHTML后,选择了后者来做引擎开发,是因为KHTML拥有清晰的源码结构和极快的渲染速度。

        需要了解的是,虽然我们称WebKit为浏览器内核(或浏览器引擎),但不太适合直接称之为我们开头提到的Rendering Engine(渲染引擎),因为WebKit本身主要是由两个引擎构成的,一个正是渲染引擎“WebCore”,另一个则是javascript解释引擎“JSCore”,它们均是从KDE的渲染引擎KHTML及javascript解释引擎KJS衍生而来。

        在2010年4月,苹果公司宣布了其浏览器引擎Webkit的最新项目 Webkit2。Webkit2的目标是实现独立进程与非阻断式API。

        WebKit可以说是苹果公司给开源世界的一大贡献,基于此开源引擎,衍生了多个WebKit分支,如下面要介绍的Chrome的浏览器引擎。

Chrome / Chromium

        谷歌Chrome/Chromium浏览器从08年创始至今一直使用苹果公司的WebKit作为浏览器内核原型,是WebKit的一个分支,我们可以称之为Chromium引擎(注意我们这里说的是Chromium引擎,而不是Chromium浏览器)。

        这里顺便介绍下Chrome和Chromium两个浏览器的区别——Chromium浏览器是谷歌为发展自家的浏览器Chrome而开启的计划,所以Chromium相当于Chrome的工程版或称实验版(尽管Chrome自身也有β版阶段),新功能会率先在Chromium上实现,待验证后才会应用在Chrome上。Chromium一天最多可以更新十几二十个版本,实验性的新特性都会现在这里放出,但是Chromium本身其实并不稳定;而Chrome总共有四个更新分支:Canary、Dev、Beta、Stable,稳定性依次增强。

        我们说回引擎。Chromium引擎虽然是属于WebKit的分支,却把WebKit的代码梳理得可读性提高很多,所以以前可能需要一天进行编译的代码,现在只要两个小时就能搞定。因此Chromium引擎和其它基于WebKit的引擎所渲染页面的效果也是有出入的。基于以上原因,有的地方会把Chromium引擎跟WebKit区分开来,有的地方则直接把Chromium引擎归为WebKit(比如维基百科),其实都有其道理。

        然而在13年发布的Chrome 28.0.1469.0版本开始,Chrome放弃Chromium引擎转而使用最新的Blink引擎(基于WebKit2——苹果公司于2010年推出的新的WebKit引擎),Blink对比上一代的引擎精简了代码、改善了DOM框架,也提升了安全性。

Opera

        Opera浏览器,是一款挪威Opera Software ASA公司制作的支持多页面标签式浏览的网络浏览器。是跨平台浏览器可以在Windows、Mac和Linux三个操作系统平台上运行。Opera浏览器创始于1995年4月,到2014年3月4日,官方发布的个人电脑用的最新版本为Opera20。

        Opera的一个里程碑作品是Opera7.0,因为它使用了Opera Software自主开发的Presto渲染引擎,取代了旧版Opera 4至6版本使用的Elektra排版引擎。

        Presto加入了动态功能,例如网页或其部分可随着DOM及Script语法的事件而重新排版。Presto在推出后不断有更新版本推出,使不少错误得以修正,以及阅读Javascript效能得以最佳化,并成为当时速度最快的引擎。

        然而为了减少研发成本,Opera在2013年2月宣布放弃Presto,转而跟随Chrome使用WebKit分支的Chromium引擎作为自家浏览器核心引擎。

        在Chrome与2013年推出Blink引擎(也是基于WebKit的分支)之后,Opera也紧跟其脚步表示将转而使用Blink作为浏览器核心引擎。

Firefox

        Mozilla Firefox是一个开源网页浏览器,原名是Firebird,2004年2月9日,Mozilla Firebird决定改称Mozilla Firefox。Firefox浏览器使用的是Gecko内核,其发展历程如下:

        1997年,网景收购了DigitalStyle。当时,网景浏览器在各方面的表现已经比不上她的主要竞争对手Internet Explorer。网景开始研发下一代的排版引擎,并期望把新的排版引擎应用于下一版本的网景浏览器上。

        1998年初,Mozilla计划开始执行。这个新的排版引擎名为Raptor,以开发源码的方式发放于互联网上。后来,因为商标问题,Raptor改名为NGLayout(即next generation layout之意)。而最后NGLayout就被网景重新命名为Gecko。

        2003年7月15日时代华纳解散了网景公司,大部分开发者被解雇。Mozilla基金会亦在当天成立,继续推动着Gecko的发展。时至今天,Gecko仍继续由Mozilla的雇员和义工所维护和发展。

        最后还是再谈谈javascript引擎(后面统称JS引擎)这东西。我们上述的渲染引擎主要是负责HTML、CSS以及其他一些东西的渲染,而JS引擎则主要负责对javascript的渲染,一个JS引擎的好坏决定了一个浏览器对脚本的加载和执行速度,也影响了其跑分。

        下方列出各种主流浏览器各自的JS引擎,了解下即可:

Firefox:

SpiderMonkey:第一款JavaScript引擎,由Brendan Eich在Netscape Communications时编写,用于Mozilla Firefox 1.0~3.0版本。

Rhino:由Mozilla基金会管理,开放源代码,完全以Java编写。

TraceMonkey:基于实时编译的引擎,其中部份代码取自Tamarin引擎,用于Mozilla Firefox 3.5~3.6版本。

JaegerMonkey:德文Jäger原意为猎人,结合追踪和组合码技术大幅提高性能,部分技术借凿了V8、JavaScriptCore、WebKit:用于Mozilla Firefox 4.0以上版本。

IonMonkey:可以对JavaScript编译后的结果进行优化,用于Mozilla Firefox 18.0以上版本。

OdinMonkey:可以对asm.js进行优化,用于Mozilla Firefox 22.0以上版本。

Chrome:

V8:开源,由Google丹麦开发,是Google Chrome的一部分。

注:我们上面提到Chrome是基于WebKit的分支,而WebKit又由渲染引擎“WebCore”和JS解释引擎“JSCore”组成,可能会让你搞不清V8和JSCore的关系。你可以这样理解——WebKit是一块主板,JSCore是一块可拆卸的内存条,谷歌实际上认为Webkit中的JSCore不够好,才自己搞了一个V8 JS引擎,这就是Chrome比Safari在某些JS测试中效率更高的原因。

IE:

Chakra:中文译名为查克拉,用于Internet Explorer 9的32位版本及IE10+。

Opera:

Linear A:用于Opera 4.0~6.1版本。

Linear B:用于Opera 7.0~9.2版本。

Futhark:用于Opera 9.5~10.2版本。

Carakan:由Opera软件公司编写,自Opera10.50版本开始使用。

其它:

KJS:KDE的ECMAScript/JavaScript引擎,最初由Harri Porten开发,用于KDE项目的Konqueror网页浏览器中。

Narcissus:开放源代码,由Brendan Eich编写(他也参与编写了第一个SpiderMonkey)。

Tamarin:由Adobe Labs编写,Flash Player 9所使用的引擎。

Nitro(原名SquirrelFish):为Safari 4编写。

字符编码-http与web编码总结

大白话之http编码总结:

1、从本质上看url编码过程:【分成path路径部分和querystring查询串部分】

scheme://host[:port#]/path/…/[url-params] (路径) [?query-string][#anchor](查询字符串,#anchor 这个是文章锚点,用于定位文章的小标题)

浏览器的问题主要是路径(pathinfo)查询字符串(querystring)的编码问题。

2、 RFC 3986(2005年出版),建议所有新的URI必须对未保留字符不加以百分号编码;其它字符建议先转换为UTF-8字节序列, 然后对其字节值使用百分号编码。此前的URI不受此标准的影响。【按道理来说,编码问题应该解决了】

但是因为历史原因,部分浏览器的url ,还没有完全采用utf-8编码。说白了大家统一认为path路径是用utf-8编码的没有问题,但是querystring 查询串该怎么编码,意见不统一。

总结来说:Path路径中需要编码的内容都是用utf-8编码的,浏览器编码问题就是querystring查询串的编码问题,因为意见不统一。

3、补充:查询串 querystring 也包括:
表单的enctype属性为application/x-www-form-urlencoded 的post请求,因为请求体中的数据也是经过urlencoded。【虽然这是旧的url编码,需要注意空格会编码成 +】这时候请求体中的数据也相当于querystring ,只是请求体中不包含?符号。

4、将path路径的二进制流    和    querystring请求串的二进制流,拼接在一起就是完整的url二进制请求流。【当然放在请求体中的请求串不算url的一部分】

5、请求头中出现Content-Type字段 ,是为了通知服务器,我有请求体。
比如: Content-Type: application/x-www-form-urlencoded
响应头中出现Content-Type字段 ,是为了通知客户端,我有响应体。
比如: Content-Type: application/json;charset=UTF-8

6、所有http流都是ASCII编码的二进制流,除了表单用post提交且编码用 multipart/form-data 的情况,这时候请求头还是ASCII流,但是请求体中上传文件是二进制原文件流,请求体中的表单元素都是按表单环境中的编码方式   编码成二进制流,至于请求体中的其他的模板内容如——WebKitFormBoundaryo2ilS97ZdtneTvV5
Content-Disposition:form-data;name=”name-file”;filename= 等等 还是用ASCII编码的二进制流


一、path路径编码详解

根据rfc3986,无论什么环境,什么浏览器,在path路径编码中,将需要的编码的字符,按utf-8编码。编码后以16进制展示,接着将16进制的展示符号,两两一组(相当于一个字节)为一个单元,在每个单元前添加百分号(%),最后将整个path进行 ASCII编码。

大白话:比如网站路径:www.baidu.com/春节
将春节:utf-8编码,E6 98 A5 E8 8A 82,然后在每个字节前添加百分号(%),即:%E6%98%A5%E8%8A%82【这算进行了一次编码,后面又会对每个字符再进行ASCII编码,变成二进制流】 

第一步网站地址 部分utf-8编码:
www.baidu.com/%E6%98%A5%E8%8A%82

第二步网站地址 全部ASCII编码后 真正的二进制流: 
77 77 77 2E 62 61 69 64 75 2E 63 6F 6D 2F 25 45 
36 25 39 38 25 41 35 25 45 38 25 38 41 25 38 32

二、querystring查询串编码详解

1、在浏览器地址栏输入的查询串

比如输入:  http://www.test.com/春节?name=春节&psw=123
所有浏览器的path路径都是用utf-8编码的,此时查询串的编码根据浏览器不同可以分为两类。

第一类:  将查询串两步编码成二进制流 ,比如 firefox,chrome,Safari,edge。
面对     ?name=春节&psw=123  。
做法是:浏览器根据自身默认的编码方式,对查询串进行编码并添加百分号。然后再继续进行第二步ASCII编码。

##### 都遵循标准url编码规范,都将查询串中需要编码的部分 采用浏览器默认的 utf-8 编码 并添加百分号
##### 编码前  ?name=春节&psw=123 
##### 编码后  ?name=%E6%98%A5%E8%8A%82&psw=123
##### ASCII码继续 编码成二进制流 
3F 6E 61 6D 65 3D 25 45 36 25 39 38 25 41 35 25 
45 38 25 38 41 25 38 32 26 70 73 77 3D 31 32 33

第二类:将查询串一步编码成二进制流,比如IE浏览器。
面对     ?name=春节&psw=123  
做法是:直接把这个  ?name=春节&psw=123  编码成二进制流,一步到位。编码方式与浏览器环境有关。中文IE 默认用 gb2312 编码方式将查询串编码。

### 中文IE 默认用 gb2312 编码查询串
###【主要是用gb2312编码 春节 二字,至于剩余的符号,因为gb2312是ASCII的一个拓展,所有用gb2312编码和ASCII编码都是一样的】
### 查询串 ?name=春节&psw=123 编码后的二进制流
3F 6E 61 6D 65 3D B4 BA BD DA 26 70 73 77 3D 31 32 33

再举一个例子:
chrome:https://wuu.wikipedia.org/wiki/%E6%98%A5%E8%8A%82?%E6%98%A5%E8%8A%82

#Request Headers

:authority: wuu.wikipedia.org
:method: GET
:path: /wiki/%E6%98%A5%E8%8A%82?%E6%98%A5%E8%8A%82
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7
cookie: WMF-Last-Access-Global=13-Aug-2018; GeoIP=JP:13:Tokyo:35.69:139.75:v4; WMF-Last-Access=13-Aug-2018
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36

我们知道,”春”和”节”的utf-8编码分别是”E6 98 A5″和”E8 8A 82″,因此,”%E6%98%A5%E8%8A%82″就是按照顺序,在每个字节前加上%而得到的。

所以 chrome浏览器,url中的 路径path 和 查询串 query string  都是UTF-8编码。其实Firefox, Safari ,Edge 都是 同样的。

##### utf-8 编码后 %E6%98%A5%E8%8A%82
##### 又经过第二次 ASCII 编码后
25 45 36 25 39 38 25 41 35 25 45 38 25 38 41 25 38 32

至于,IE 浏览器 无论 IE 8 还是 IE 11 ,路径 path是UTF-8 编码,【查询串queryString 则按照 浏览器默认 编码来进行。比如 中文的IE 用的 就是 GB2312编码。 而且是一步到位,直接编码成二进制流,没有百分号编码的过程。】


IE浏览器:www.baidu.com/春节?春节

#Request Headers

GET /%E6%98%A5%E8%8A%82?���� HTTP/1.1
Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*
Accept-Language: zh-cn
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Host: www.baidu.com
Cookie: BAIDUID=DFF7C9EB333036D329E7EB55757711B7:FG=1; BIDUPSID=D05CF78322A0985BC8FD928169BC4BF1; PSTM=1533825584; H_PS_PSSID=; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BD_UPN=1123314151; H_PS_645EC=9c51wK0QsNr9o%2BoxCNqWR7faZJlxa6F7GdNch0OwzQznYCdO%2B5jhAlHofJh5PLalplC3%2BoaOqoNeFg

我们知道,”春”和”节”的GB2312编码(我的操作系统”Windows XP”中文版的默认编码)分别是”B4 BA”和”BD DA”。因此,IE实际上就是将查询字符串,以GB2312编码的格式发送出去。至于,路径pathinfo 还是 用utf-8  的形式,比如春节两字的编码– %E6%98%A5%E8%8A%82【但是IE的查询串没有用%编码,直接GB2312编码成二进制流,与其他浏览器先百分号编码,再编码成二进制流不同】

=======================================================

参考其他文章,说以前Firefox的 pathInfo和queryString都是URL encode按照GB2312编码的。对于中文IE,如果在高级选项中选中总以UTF-8发送(默认方式),则PathInfo是URL Encode是按照UTF-8编码,QueryString是按照GBK编码。 很显然,不同的浏览器以及同一浏览器的不同设置,会影响最终URL中PathInfo的编码。

按照RFC3986 的规定,需要编码的字符,都应该用utf-8编码成字节流,然后百分号编码。所以这是浏览器的历史遗留问题。

根据现在的浏览器情况:浏览器地址栏直接输入:

Chrome,Safari,Edge,Firefox,pathInfo和query string 都是utf-8编码
IE pathInfo 用utf-8 编码,query string 用 gb2312 编码


2、点击超链接生成的查询串

比如网页中有如下超链接:
<a href="gbk-response/春节?name=春节&psw=123">go</a>

所有浏览器的path路径都是用utf-8编码的,此时查询串的编码根据浏览器不同可以分为两类。

第一类:  将查询串两步编码成二进制流 ,比如 firefox,chrome,Safari,edge。
面对     ?name=春节&psw=123  。
做法是:
1、浏览器首先根据网页中设置的编码方式对查询串进行编码并添加百分号。
  <meta http-equiv="content-type" content="text/html;charset=utf-8">
2、如果网页中找不到指定的编码方法,浏览器就会采用
自身默认的编码方式,对查询串进行编码并添加百分号(中文环境下firefox,chrome,Safari,edge,默认都是utf-8 编码),然后再继续对查询串进行第二步ASCII编码。

第二类:将查询串一步编码成二进制流,比如IE浏览器。
面对     ?name=春节&psw=123  
做法是:
1、浏览器首先根据网页中设置的编码方式对查询串一步到位编码成二进制流。
  <meta http-equiv="content-type" content="text/html;charset=utf-8">
2、如果网页中找不到指定的编码方法,浏览器就会采用
自身默认的编码方式(中文环境下 IE 默认采用gb2312编码)一步到位编码成二进制流。


3、表单提交生成的查询串

如果请求串,由表单提交生成且表单的enctype属性为 application/x-www-form-urlencoded【默认属性】,无论是get还是post方法,都会进行百分号编码【当然这种旧url编码需要注意 空格会编码成 +】

form元素有个enctype属性,可以指定数据编码方式,有如下三种:

1. application/x-www-form-urlencoded: 表单数据编码为键值对,&分隔
2. multipart/form-data: 表单数据编码为一条消息,每个控件对应消息的一部分
3. text/plain: 表单数据以纯文本形式进行编码

详细说明:

form的enctype的编码方式,常用有两种:
application/x-www-form-urlencoded和multipart/form-data
其中 application/x-www-form-urlencoded为默认编码方式。

在form的action为get时,浏览器用x-www-form-urlencoded的编码方式,将表单数据编码为
(name1=value1&name2=value2...),然后把这个字符串append到url后面,用?分隔,跳转
到这个新的url
当form的action为post时,浏览器将form数据封装到http body中,然后发送到server。
在没有type=file时候,用默认的 application/x-www-form-urlencoded 就行。
在有 type=file 时候,要用multipart/form-data编码方式。浏览器会把表单以控件为单位分割,
并且为每个部分加上Content-Dispositon(form-data或file)、Content-Type(默认text/plain)、
name(控件name)等信息,并加上分割符(boundary)。

假设浏览器所在环境,采用UTF-8编码将表单信息编码成querystring【编码选择方式后文会讲到】
1、如果浏览器用get方式请求   http://www.test.com/春节?name=春节&psw=123
则url显示结果为:http://www.test.com/%E6%98%A5%E8%8A%82?name=%B4%BA%BD%DA&pwd=123
2、如果浏览器是post请求方式:则url显示结果为http://www.test.com/%E6%98%A5%E8%8A%82
且请求体中有:name=%B4%BA%BD%DA&pwd=123

不管是放在url中还是请求体中,接下来仍需要将请求串ASCII编码。

### 已经过一次编码的请求串
?name=%E6%98%A5%E8%8A%82&psw=123
### ASCII码继续 编码成二进制流
3F 6E 61 6D 65 3D 25 45 36 25 39 38 25 41 35
25 45 38 25 38 41 25 38 32 26 70 73 77 3D 31
### 注意放在请求体中的请求串(严格来讲已不叫请求串了)
### 因为请求体中的请求串没有问号(?)所以请求体的二进制流要去掉第一个3F

4、表单提交生成的查询串的编码选择

(1)如果网页中 的head标签中 没有指定 网页编码类型

比如缺少类似下面这个的

  <meta http-equiv="content-type" content="text/html;charset=utf-8">

那么浏览器会使用默认的编码方式进行解码,这样会出现乱码的可能。比如mac下的chrome浏览器,我这边测试出来默认编码是用gb2312的。如果网页没直接指明编码方式,那么如果是gb2312编码的网页那会显示正确。如果是utf-8编码的网页,网页就会解析错误,产生乱码。因为浏览器默认是用GB2312解码网页的。

测试了一下Safari,发现浏览器默认编码即不是utf-8,也不是GB2312。
测试了一下Firefox,浏览器默认编码是GB2312。
测试了一下IE,浏览器默认编码是GB2312。

注:浏览器默认编码可能和系统默认编码有关。【如中文系统和英文系统】

(2)如果表单中,指定了accept-charset

网页里的form编码其实不完全取决于网页编码,form标记中有一个accept-charset属性,在非ie浏览器种,如果将其赋值(比如accept-charset=”UTF-8″),则表单会按照这个值表示的编码方式,将需要编码的字符编码成字节流,然后采用  application/x-www-form-urlencoded   非标准URL编码。

<form action="form_action.asp" accept-charset="utf-8">
  <p>First name: <input type="text" name="fname" /></p>
  <p>Last name: <input type="text" name="lname" /></p>
  <input type="submit" value="Submit" />
</form>

定义和用法

accept-charset 属性规定服务器处理表单数据所接受的字符集。此属性的默认值是 “unknown”,表示表单的字符集与包含表单的文档的字符集相同。

提示:请避免使用该属性。应该在服务器端验证文件上传。

浏览器支持

除了 Internet Explorer,accept-charset 属性得到几乎所有浏览器的支持。

注释:accept-charset 属性无法在 Internet Explorer 中正确地工作。如果 accept-charset 属性设置为 “ISO-8859-1″,IE 将发送以 “Windows-1252” 编码的数据。

我在chrome浏览器中测试过,gb2312编码的网页,表单用指定用utf-8编码。

结果我发现,请求头中 Content-Type: application/x-www-form-urlencoded   也不存在 charset=xxx 的字段。

POST http://192.168.2.233/http_encode/gbk-response/%E6%98%A5%E8%8A%82 HTTP/1.1
Host: 192.168.2.233
Connection: keep-alive
Content-Length: 32
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://192.168.2.233
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Referer: http://192.168.1.133/~cool/http_encode/gbk-post.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8

name=%E6%98%A5+%E8%8A%82&pwd=123

 

在ie下,我的兼容解决办法是:

form1.onsubmit=function(){
document.charset=this.getAttribute('accept-charset');
}

三、表单知识补充

1、四种常见的 POST 提交数据方式

(1)application/x-www-form-urlencoded
(2)multipart/form-data
(3)application/json
(4)text/xml(xml-rpc)

2、表单提交方式GET与POST的区别

当用GET提交时,会将表单信息变成query string 查询字符串。根据 网页中 head 标签 里面设定的编码方案  将   query string 中 需要编码的字符  编码成字节流 。然后再用非标准的url编码。【大白话,GET提交表单时,表单信息变成了URL里面的query string,即在http协议的请求行中】

因为 query string  在表单 中 ,采用的 是  application/x-www-form-urlencoded   非标准URL编码。 这和标准的URL编码有区别。比如编码空格时标准url编码成”%20″,非标准url编码成”+”。

========================================================

当用POST提交表单时,会将表单信息变成FORM DATA,存放在http协议中的 请求体中。FORM DATA 和query string 一样,也是将 需要编码的部分,根据网页head中设定的编码方式,编码成字节流。然后采用  application/x-www-form-urlencoded   非标准URL编码。

同时,会在请求头中 加入:

Content-Type: application/x-www-form-urlencoded

注意:无论是GET还是POST方式,将表单信息编码成字节流时,空格是不需要编码的,所以后面采用  application/x-www-form-urlencoded   非标准URL编码时,空格 一定会被编码成 “+”符号。

3、表单中的enctype属性

表单常用属性:

action:url 地址,服务器接收表单数据的地址
method:提交服务器的http方法,一般为post和get
name:参数名属性

######################################
enctype: 表单数据提交时使用的编码类型,默认使用"aplication/x-www-form-urlencoded",
如果是使用POST请求,则请求头中的content-type指定值就是该值。因为包含了请求体。
如果是使用GET请求,通过URL的query string传递参数,没有请求体,所以不需要content-type
######################################

如果表单中有上传文件,编码类型需要使用"multipart/form-data",类型,才能完成传递文件数据。
<form action="/upload" enctype="multipart/form-data" method="post">
    Username: <input type="text" name="username">
    Password: <input type="password" name="password">
    File: <input type="file" name="file">
    <input type="submit">
</form>

4、表单用post提交,并且编码用 multipart/form-data 的情况

这种情况下,除了上传的文件直接采用二进制流没有再编码,其他的表单信息编码方式和x-www-form-urlencoded一样。举例如下:
表单信息:【name-desc 填写  春节  二字,上传的文件为   春节.txt】

<form action="testfileupload" method="post" enctype="multipart/form-data" accept-charset="gb2312">
File:<input type="file" name="name-file"/>
Desc:<input type="text" name="name-desc"/>
<input type="submit" name="name-submit" value="submit" />
</form>

待上传文件:春节.txt
文件内容:

good春节OK
hello

文件二进制代码:
【备注在Windows的记事本软件中如果用utf-8编码保存时,会自动在保存信息中添加EF BB BF 字节序,而mac系统下不会,这里实验采用mac系统】

676f 6f64 e698 a5e8 8a82 4f4b 0d0a 6865
6c6c 6f

提交请求时,fiddler2抓包结果:

POST http://192.168.1.131:8080/springmvc-2/testfileupload HTTP/1.1
Host: 192.168.1.131:8080
Connection: keep-alive
Content-Length: 410
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://192.168.1.131:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryo2ilS97ZdtneTvV5
Referer: http://192.168.1.131:8080/springmvc-2/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Cookie: JSESSIONID=A15ECBF73D297496D7785129A231A321

------WebKitFormBoundaryo2ilS97ZdtneTvV5
Content-Disposition: form-data; name="name-file"; filename="    .txt"
Content-Type: text/plain

good春节OK
hello
------WebKitFormBoundaryo2ilS97ZdtneTvV5
Content-Disposition: form-data; name="name-desc"

    
------WebKitFormBoundaryo2ilS97ZdtneTvV5
Content-Disposition: form-data; name="name-submit"

submit
------WebKitFormBoundaryo2ilS97ZdtneTvV5--

http请求特别解析:

##### 下面的  filename="    .txt"   其实是   filename="春节.txt"
##### 之所有 出现空白是表单信息中 春节 2个字 二进制信息是 B4 BA BD DA ,采用了GBK编码
##### 而 fiddler2 显示信息 用的是 utf-8 编码 所有显示不出来
Content-Disposition: form-data; name="name-file"; filename="    .txt"

##### 下面第三行默认是空白,而第四行 也是 空白 但是 有二进制信息 是 B4 BA BD DA
------WebKitFormBoundaryo2ilS97ZdtneTvV5
Content-Disposition: form-data; name="name-desc"

如果把表单 删除gb2312编码,也就是用 网页内定 的utf-8 编码:

<form action="testfileupload" method="post" enctype="multipart/form-data" >
File:<input type="file" name="name-file"/>
Desc:<input type="text" name="name-desc"/>
<input type="submit" name="name-submit" value="submit" />
</form>

抓包结果:

POST http://192.168.1.131:8080/springmvc-2/testfileupload HTTP/1.1
Host: 192.168.1.131:8080
Connection: keep-alive
Content-Length: 414
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://192.168.1.131:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4v5NL53bCqC2d9fH
Referer: http://192.168.1.131:8080/springmvc-2/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Cookie: JSESSIONID=A15ECBF73D297496D7785129A231A321

------WebKitFormBoundary4v5NL53bCqC2d9fH
Content-Disposition: form-data; name="name-file"; filename="春节.txt"
Content-Type: text/plain

good春节OK
hello
------WebKitFormBoundary4v5NL53bCqC2d9fH
Content-Disposition: form-data; name="name-desc"

春节
------WebKitFormBoundary4v5NL53bCqC2d9fH
Content-Disposition: form-data; name="name-submit"

submit
------WebKitFormBoundary4v5NL53bCqC2d9fH--

从这里我们能够看到,所有 春节 两字 编码信息 都是 utf-8 编码,都能在fiddler2 抓包中,正确显示。

从结果比较来看, multipart/form-data 的请求体中,上传文件的表单单元,不需要再编码,直接传输文件二进制流,因为春节.txt是utf-8编码,而fiddler2默认是用utf-8解码,所有解码正确。至于其他的表单单元,根据表单环境提供的编码方式编码,然后传递。上面的例子,表单第一次编码是gb2312,而fiddler2是utf-8解码,导致抓包时,信息解码错误。表单第二次编码是utf-8,和fiddler2的解码一致,所以信息抓包解码正确。

======================================================

特别说明:如果改成了iso-8859-1的编码表单

<form action="testRequestBody" method="post" enctype="multipart/form-data" accept-charset="iso-8859-1">
File:<input type="file" name="name-file"/>
Desc:<input type="text" name="name-desc"/>
<input type="submit" name="name-submit" value="submit" />
</form>

上传文件名还是春节.txt ,表单输入的name-desc 也还是 春节

这个时候fiddler2抓包二进制流,然后用utf-8解码结果:

POST http://192.168.1.131:8080/springmvc-2/testRequestBody HTTP/1.1
Host: 192.168.1.131:8080
Connection: keep-alive
Content-Length: 420
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://192.168.1.131:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary3FCh7yh4z2wpPZo9
Referer: http://192.168.1.131:8080/springmvc-2/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Cookie: JSESSIONID=07D6AF477CEE3B7CDB6A1847E2ECB3A4

------WebKitFormBoundary3FCh7yh4z2wpPZo9
Content-Disposition: form-data; name="name-file"; filename="??.txt"
Content-Type: text/plain

good春节OK
hello
------WebKitFormBoundary3FCh7yh4z2wpPZo9
Content-Disposition: form-data; name="name-desc"

&#26149;&#33410;
------WebKitFormBoundary3FCh7yh4z2wpPZo9
Content-Disposition: form-data; name="name-submit"

submit
------WebKitFormBoundary3FCh7yh4z2wpPZo9--

我们看到:请求体中
文件名为: filename=”??.txt”    查看二进制发现春节两字 编码成  3F 3F
而 表单中 name-desc 值为:  &#26149;&#33410;    【这个26149 33410 分别是春节的Unicode 十进制 值】【&#26149;&#33410; 这两个都可以用ASCII解码成二进制流比如 &#代表26 23】
从这里我们能够看出来:表单用 multipart/form-data 传递信息时,当遇到表单信息无法编码时,表单中的不同类型,应对结果可能不同。比如 表单中的文件名应对错误编码的结果与 表单中 name-desc 应对错误编码的结果不同。但是,如果表单能被正确编码,那么两者编码结果值相同(因为两者原码相同)。

后来,我们又将表单编码设置成立utf-16,但是抓包结果来看,表单信息还是被编码成了utf-8,所以感觉没有效果。

当我们将表单 用big5 编码时,上传文件名 改为:春節   ,表单填写name-desc时 也改为 春節 ,抓包结果看到这两处 春節 都被编码成了   AC 4B B8 60  二进制流。至于文件中的信息春节,因为文件用utf-8编码过的二进制流,直接放入请求体中,不需要二次编码,所以不受表单编码的影响。

我们知道的是表单信息提交时,会按照表单设置的编码进行发送。但是对于 上传文件,用的是原始二进制流,不用编码。现在有一个疑问,就是http请求中,默认格式字符信息,用的是ASCII编码  还是 用表单设置的编码,目前我看到的表单编码都是兼容ASCII码的(比如UTF-8,GBK,big5 兼容ASCII),所以无法实质区分。曾尝试用utf-16设置表单编码,但是抓包后发现,表单信息居然采用了utf-8 编码。所以区分不了。


四、url编码与javascript

url编码-JavaScript-函数

   escape 、 encodeURI 、encodeURIComponent

首先,无论网页的编码方式是什么,都可以正确解析,毕竟编码和解码是对应统一的。JavaScript引擎根据 已经编码好的网页二进制流 和 网页的编码方式 , 将网页二进制信息流正确转码成JavaScript引擎内部能处理的二进制流【内部编码要么是ucs-2 或者是 utf -16,待探究】

所以,JavaScript 中的  函数参数 也会 编码成 内部编码。【JavaScript内部编码可能是 ucs-2,utf-16,总之:两个字节内,编码的结果都是一样的,以后再探讨到底是哪种编码 】

javascript:escape("\u6625\u8282");  ==>  反斜杠 \ 代表Unicode转义字符。以Unicode字符序的方式接收
javascript:escape("春节");  ==>  春节 二字,代表默认的字符 接收
#这里的函数参数  春节  或者 Unicode排序值  都将内部都编码成了 UCS-2 或者 UTF-16  
#这里的函数返回值 就是 将内部编码后的数据 进行运算,并将运算结果转码成 Unicode排序值,然后输出

编码问题归纳:记录网页信息的网页编码 和 JavaScript引擎处理网页信息的内部编码。

下面列出了这三个函数的安全字符(即函数不会对这些字符进行编码)

escape(69个):*/@+-._0-9a-zA-Z
encodeURI(82个):!#$&'()*+,/:;=?@-._~0-9a-zA-Z
encodeURIComponent(71个):!'()*-._~0-9a-zA-Z
encodeURI('https://www.baidu.com/ a b c')
// "https://www.baidu.com/%20a%20b%20c"
encodeURIComponent('https://www.baidu.com/ a b c')
// "https%3A%2F%2Fwww.baidu.com%2F%20a%20b%20c"
 
//而 escape 会编码成下面这样,eocode 了冒号却没 encode 斜杠,十分怪异,故废弃之
escape('https://www.baidu.com/ a b c')
// "https%3A//www.baidu.com/%20a%20b%20c"

实际上,escape()不能直接用于URL编码,它的真正作用是返回一个字符的Unicode排序值。比如”春节”的返回结果是%u6625%u8282,也就是说在Unicode字符集中,”春”是第6625个(十六进制)字符,”节”是第8282个(十六进制)字符。

春的Unicode排序值为:6625(十六进制)   ; 26149(十进制)   

春的utf-8编码值为:E6 98 A5(十六进制) ;URL编码采用的是utf-8 编码!!!

Javascript:escape("春节");
"%u6625%u8282"

Javascript:escape("Hello World");
"Hello%20World"

###  反斜杠 \ 代表Unicode转义字符 ,以Unicode字符序的方式接收。

javascript:escape("\u6625\u8282");
"%u6625%u8282"

javascript:unescape("%u6625%u8282");
"春节"

javascript:unescape("\u6625\u8282");
"春节"

 

escape()的具体规则是,除了ASCII字母、数字、标点符号”@ * _ + – . /”以外,对其他所有字符进行编码。在\u0000到\u00ff之间的符号被转成%xx的形式,其余符号被转成%uxxxx的形式。对应的解码函数是unescape()。

escape()不对”+”编码。但是我们知道,网页在提交表单的时候,如果有空格,则会被转化为+字符。服务器处理数据的时候,会把+号处理成空格。所以,使用的时候要小心。

escape函数已经被W3C废弃了。因为 escape函数 返回的是Unicode 排序值,不是 utf-8 编码值。不符合RFC3986 标准。但是在ECMA-262标准中仍然保留着escape的这种编码语法。


encodeURI()是Javascript中真正用来对URL编码的函数。

它着眼于对整个URL进行编码,因此除了常见的符号以外,对其他一些在网址中有特殊含义的符号”; / ? : @ & = + $ , #”,也不进行编码。编码后,它输出符号的utf-8形式,并且在每个字节前加上%。

encodeURI("http://www.cnblogs.com/season-huang/some other thing");
//"http://www.cnblogs.com/season-huang/some%20other%20thing";

编码后变为上述结果,可以看到空格被编码成了%20,而斜杠 / ,冒号 : 并没有被编码。需要注意的是,它不对单引号’编码。


encodeURIComponent()。与encodeURI()的区别是,它用于对URL的组成部分进行个别编码,而不用于对整个URL进行编码。

因此,”; / ? : @ & = + $ , #”,这些在encodeURI()中不被编码的符号,在encodeURIComponent()中统统会被编码。至于具体的编码方法,两者是一样。

使用场景:当我们的 URL 长这样子,请求参数中带了另一个 URL :

var URL = "http://www.a.com?foo=http://www.b.com?t=123&s=456";
//直接对它进行 encodeURI 显然是不行的。
//因为 encodeURI 不会对冒号 : 及斜杠 / 进行转义,
//那么就会出现上述所说的服务器接受到之后解析会有歧义。

encodeURI(URL);
// "http://www.a.com?foo=http://www.b.com?t=123&b=456"
//这个时候,就该用到 encodeURIComponent() 。
//它的作用是对 URL 中的参数进行编码,记住是对参数,而不是对整个 URL 进行编码。

//正确的用法:encodeURIComponent() 着眼于对单个的参数进行编码:
var param = "http://www.b.com?t=123&s=456"; // 要被编码的参数
URL = "http://www.a.com?foo="+encodeURIComponent(param);
//"http://www.a.com?foo=http%3A%2F%2Fwww.b.com%3Ft%3D123%26s%3D456"

url编码-JavaScript-AJAX

GBK编码的gbk-get.html

<!DOCTYPE html>
<html>
<head>
    <title>gb2312网页测试get提交数据方法</title>
    <meta http-equiv="content-type" content="text/html;charset=gb2312">
    <script type="text/javascript"	src="jquery.js"></script>
</head>
<body>
    <center>
      <a href="gbk-response/春节?name=春节&psw=123">go</a>
        <form action="gbk-http-get-response/春节" method="get">
            <p>用户名:<input type="text" id="name"  name="name"/></p>
            <p>密码:<input type="password" id="pwd" name="pwd"/></p>
            <input type="submit" value="登录">
            <input type="button" value="ajax-get" id="ajax-get-btn"/>
            <input type="button" value="ajax-post" id="ajax-post-btn"/>

        </form>
    </center>

<script>

    $("#ajax-get-btn").click(function(){
    			//发送ajax请求,弹出部门信息,显示在下拉列表中
          $.ajax({
            url:"gbk-ajax-get-response/春节",
            type:"get",
            data:"name="+$("#name").val(),
            success:function(result){

            }

          })
    		});


        $("#ajax-post-btn").click(function(){

              //获取DOM对象的 value 属性
              var name = $("#name")[0].value;

          		//发送ajax请求,弹出部门信息,显示在下拉列表中
              $.ajax({
                url:"gbk-ajax-post-response/春节",
                type:"post",
                //下面这个 ${"#name"}.value 写错了,难怪一直报错。 应该是 $("#name").value
                //data:"name="+${"#name"}.value,
                data:"name="+name,
                success:function(result){

                }

              })
        		});

            // $("#ajax-get-btn").click(function(){
            //
            //     var name = ${"#name"}.value;
            //
            //       //发送ajax请求,弹出部门信息,显示在下拉列表中
            //       $.ajax({
            //         url:"control",
            //         type:"get",
            //       //  下面的写法是错误的,正确的是 1、获取DOM属性  data:"name="+$("#name")[0].value,  或者 2、获取JQuery属性   data:"name="+$("#name").val(),
            //       //  data:"name="+${"#name"}.value,
            //         data:"name="+name,
            //         success:function(result){
            //
            //         }
            //
            //       })
            //     });

</script>

</body>
</html>

utf-8编码的文件大致相同:

测试结果:

fiddler2 用 utf-8 解码显示 http请求信息

IE浏览器,gbk网页,用get请求

GET http://192.168.1.131/~sky/http_encode/gbk-ajax-get-response/%B4%BA%BD%DA?name=     HTTP/1.1
x-requested-with: XMLHttpRequest
Accept-Language: zh-cn
Referer: http://192.168.1.131/~sky/http_encode/gbk-get.html
Accept: */*
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
Host: 192.168.1.131
Connection: Keep-Alive

/%B4%BA%BD%DA?name=
上面用utf-8显示的文字,用16进制表示如下:
2F 25 42 34 25 42 41 25 42 44 25 44 41 3F 6E 61 6D 65 3D B4 BA BD DA

IE浏览器,gbk网页,用post请求
POST http://192.168.1.131/~sky/http_encode/gbk-ajax-post-response/%B4%BA%BD%DA HTTP/1.1
x-requested-with: XMLHttpRequest
Accept-Language: zh-cn
Referer: http://192.168.1.131/~sky/http_encode/gbk-get.html
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
Host: 192.168.1.131
Content-Length: 11
Connection: Keep-Alive
Pragma: no-cache

name=春节

fiddler2 用utf-8 解码http信息:
请求体: name=春节  的二进制码为 6e 61 6d 65 3d e6 98 a5 e8 8a 82


IE浏览器 utf-8 网页 ,用get请求

GET http://192.168.1.131/~sky/http_encode/utf8-ajax-get-response/%B4%BA%BD%DA?name=     HTTP/1.1
x-requested-with: XMLHttpRequest
Accept-Language: zh-cn
Referer: http://192.168.1.131/~sky/http_encode/utf-8-get.html
Accept: */*
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
Host: 192.168.1.131
Connection: Keep-Alive

/%B4%BA%BD%DA?name=
上面用utf-8显示的文字,用16进制表示如下:
2F 25 42 34 25 42 41 25 42 44 25 44 41 3F 6E 61 6D 65 3D B4 BA BD DA


IE浏览器 utf-8 网页 ,用post请求

POST http://192.168.1.131/~sky/http_encode/utf8-ajax-post-response/%B4%BA%BD%DA HTTP/1.1
x-requested-with: XMLHttpRequest
Accept-Language: zh-cn
Referer: http://192.168.1.131/~sky/http_encode/utf-8-get.html
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
Host: 192.168.1.131
Content-Length: 11
Connection: Keep-Alive
Pragma: no-cache

name=春节

fiddler2 用utf-8 解码http信息:
请求体: name=春节  的二进制码为 6e 61 6d 65 3d e6 98 a5 e8 8a 82
=================================================================

chrome gbk网页 get请求

GET /~sky/http_encode/gbk-ajax-get-response/%E6%98%A5%E8%8A%82?name=%B4%BA%BD%DA HTTP/1.1
Host: localhost
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Referer: http://localhost/~sky/http_encode/gbk-get.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7

chrome gbk网页 post请求

POST /~sky/http_encode/gbk-ajax-post-response/%E6%98%A5%E8%8A%82 HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 11
Accept: */*
Origin: http://localhost
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://localhost/~sky/http_encode/gbk-get.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7

name=春节

抓包发现:请求体的二进制代码为:
6e 61 6d 65 3d e6 98 a5 e8 8a 82



chrome utf-8 get请求

GET /~sky/http_encode/utf8-ajax-get-response/%E6%98%A5%E8%8A%82?name=%E6%98%A5%E8%8A%82 HTTP/1.1
Host: localhost
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Referer: http://localhost/~sky/http_encode/utf-8-get.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7

chrome utf-8 post请求

POST /~sky/http_encode/utf8-ajax-post-response/%E6%98%A5%E8%8A%82 HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 11
Accept: */*
Origin: http://localhost
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://localhost/~sky/http_encode/utf-8-get.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7

name=春节
抓包发现:请求体的二进制代码为:
6e 61 6d 65 3d e6 98 a5 e8 8a 82

也就是说,在Ajax调用中,path路径调用不同,IE是%B4%BA%BD%DA,chrome是%E6%98%A5%E8%8A%82querystring 又和网页的编码有关系,IE 是 一步到位的编码。chrome是两步到位的编码,中间经历了百分号编码。


五、url编码与服务器响应

Apache服务器返回的   Response Headers :

HTTP/1.1 200 OK
Date: Wed, 15 Aug 2018 07:49:34 GMT
Server: Apache/2.4.28 (Unix) PHP/5.5.38
Last-Modified: Wed, 15 Aug 2018 03:41:28 GMT
ETag: "1c2-xxxxxxxxxxxx"  【一串字符串】
Accept-Ranges: bytes
Content-Length: 450
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html【我看到百度Apache服务器,这里是 text/html;charset=utf-8】

百度Apache 服务器 Request Headers

HTTP/1.1 200 OK
Bdpagetype: 3
Bdqid: XXXXXXXXXXXXXXXX
Cache-Control: private
Content-Encoding: gzip
Content-Type: text/html;charset=utf-8
Cxy_all: news+XXXXXXXXXXXXXXXXXXXXXXXXX
Cxy_ex: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Date: Wed, 15 Aug 2018 07:43:23 GMT
P3p: CP=" OTI DSP COR IVA OUR IND COM "
Server: Apache
Tracecode: XXXXXXXXXXXXXXXXXXXXXXXXX
Vary: Accept-Encoding
Vary: Accept-Encoding
X-Ua-Compatible: IE=Edge,chrome=1

Nginx 服务器 Request Headers:

HTTP/1.1 200 OK
Server: nginx/1.14.0
Date: Wed, 15 Aug 2018 07:56:25 GMT
Content-Type: text/html
【上面是欢迎页的响应,我个人网站和Nginx官网服务器的响应都是text/html;charset=utf-8】
Content-Length: 612
Last-Modified: Tue, 17 Apr 2018 15:48:00 GMT
Connection: keep-alive
ETag: "XXXXXXXXXX-XXX"  【一串字符串】
Accept-Ranges: bytes

nginx官网服务器:Response Headers

content-encoding: gzip
content-type: text/html; charset=UTF-8
date: Wed, 15 Aug 2018 08:22:48 GMT
link: <https://www.nginx.com/wp-json/>; rel="https://api.w.org/"
link: <https://www.nginx.com/>; rel=shortlink
link: <https://www.nginx.com/wp-json>; rel="https://github.com/WP-API/WP-API"
server: nginx
status: 200
vary: Accept-Encoding, User-Agent
x-cache-config: 0 0
x-cache-status: HIT
x-cache-status: HIT
x-pingback: https://www.nginx.com/xmlrpc.php
x-user-agent: standard

Tomcat 服务器解析 http 请求过程

1、开发人员必须清楚的servlet规范:

(1) HttpServletRequest.setCharacterEncoding()方法 仅仅只适用于设置post提交的request body的编码而不是设置get方法提交的queryString的编码。该方法告诉应用服务器应该采用什么编码解析post传过来的内容。很多文章并没有说明这一点。
(2) HttpServletRequest.getPathInfo()返回的结果是由Servlet服务器解码(decode)过的。
(3) HttpServletRequest.getRequestURI()返回的字符串没有被Servlet服务器decoded过。
(4) POST提交的数据是作为request body的一部分。
(5) 网页的Http头中ContentType(“text/html; charset=GBK”)的作用:
(a) 告诉浏览器网页中数据是什么编码;
(b) 表单提交时,通常浏览器会根据ContentType指定的charset对表单中的数据编码,然后发送给服务器的。
这里需要注意的是:这里所说的ContentType是指http头的ContentType,而不是在网页中meta中的ContentType。

2、Java中的uri 和 url 【和IETF设置的标准不同】

浏览器请求:http://localhost:8080/springmvc-1/春节.jsp?p=春节

写了一个 春节.jsp 文件

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page language="java" import="java.util.*" %>
    
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

测试完成 uri 与 url
<br>


<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";

out.println("basePath:"+basePath);
out.println("<br/>");
out.println("getContextPath:"+request.getContextPath());
out.println("<br/>");
out.println("getServletPath:"+request.getServletPath());
out.println("<br/>");
out.println("getRequestURI:"+request.getRequestURI());
out.println("<br/>");
out.println("getRequestURL:"+request.getRequestURL());
out.println("<br/>");
out.println("getRealPath:"+request.getRealPath("/"));
out.println("<br/>");
out.println("getServletContext().getRealPath:"+getServletContext().getRealPath("/"));
out.println("<br/>");
out.println("getQueryString:"+request.getQueryString());

%>
	
</body>
</html>

结果显示:chrome,Safari,Firefox,Edge结果

测试完成 uri 与 url 
basePath:http://localhost:8080/springmvc-1/ 
getContextPath:/springmvc-1 
getServletPath:/春节.jsp 
getRequestURI:/springmvc-1/%E6%98%A5%E8%8A%82.jsp 
getRequestURL:http://localhost:8080/springmvc-1/%E6%98%A5%E8%8A%82.jsp 
getRealPath:/Users/hello/Documents/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/springmvc-1/ 
getServletContext().getRealPath:/Users/hello/Documents/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/springmvc-1/ 
getQueryString:p=%E6%98%A5%E8%8A%82

IE浏览器显示结果【浏览器地址栏 编码,query string  用了GB2312 导致乱码】

测试完成 uri 与 url 
basePath:http://192.168.1.133:8080/springmvc-1/ 
getContextPath:/springmvc-1 
getServletPath:/春节.jsp 
getRequestURI:/springmvc-1/%E6%98%A5%E8%8A%82.jsp 
getRequestURL:http://192.168.1.133:8080/springmvc-1/%E6%98%A5%E8%8A%82.jsp 
getRealPath:/Users/hello/Documents/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/springmvc-1/ 
getServletContext().getRealPath:/Users/hello/Documents/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/springmvc-1/ 
getQueryString:p=´º½Ú

Tomcat 服务器 response 报文:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=UTF-8
Content-Length: 849
Date: Thu, 16 Aug 2018 09:50:30 GMT

java中 request.getServletPath() 和 request.getPathInfo() 的区别 {映射问题造成}

在 Web 中,我们通常需要获取 URL 相对于 Webapp 的路径,主要是下面的几个方法:

request.getServletPath()
request.getPathInfo()
request.getContextPath()
request.getRequestURI()

其中 request.getRequestURI() 的返回值包含了 request.getContextPath(),所以是相对于网站的根目录的。

下面我们分析 request.getServletPath() 和 request.getPathInfo()

1. 如果我们的 servlet-mapping 如下配置:

<servlet-mapping>

  <servlet-name>jetbrick-template</servlet-name>

  <url-pattern>*.jetx</url-pattern>

</servlet-mapping>

那么访问: /context/templates/index.jetx

request.getServletPath() == "/templates/index.jetx"

request.getPathInfo() == <null>

2. 如果我们的 servlet-mapping 如下配置:

<servlet-mapping>

  <servlet-name>jetbrick-template</servlet-name>

  <url-pattern>/*</url-pattern>

</servlet-mapping>

那么访问: /context/templates/index.jetx

request.getServletPath() == ""

request.getPathInfo() == "/templates/index.jetx"

3. 如果我们的 servlet-mapping 如下配置:

<servlet-mapping>

  <servlet-name>jetbrick-template</servlet-name>

  <url-pattern>/template/*</url-pattern>

</servlet-mapping>

那么访问: /context/templates/index.jetx

request.getServletPath() == "/templates"
request.getPathInfo() == "/index.jetx"
3、tomcat 解析http 流的过程

解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到 org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的:

protected void convertURI(MessageBytes uri, Request request) 
throws Exception { 
       ByteChunk bc = uri.getByteChunk(); 
       int length = bc.getLength(); 
       CharChunk cc = uri.getCharChunk(); 
       cc.allocate(length, -1); 
       String enc = connector.getURIEncoding(); 
       if (enc != null) { 
           B2CConverter conv = request.getURIConverter(); 
           try { 
               if (conv == null) { 
                   conv = new B2CConverter(enc); 
                   request.setURIConverter(conv); 
               } 
           } catch (IOException e) {...} 
           if (conv != null) { 
               try { 
                   conv.convert(bc, cc, cc.getBuffer().length - 
cc.getEnd()); 
                   uri.setChars(cc.getBuffer(), cc.getStart(), 
cc.getLength()); 
                   return; 
               } catch (IOException e) {...} 
           } 
       } 
       // Default encoding: fast conversion 
       byte[] bbuf = bc.getBuffer(); 
       char[] cbuf = cc.getBuffer(); 
       int start = bc.getStart(); 
       for (int i = 0; i < length; i++) { 
           cbuf[i] = (char) (bbuf[i + start] & 0xff); 
       } 
       uri.setChars(cbuf, 0, length); 
}

从上面的代码中可以知道对 URL 的 URI【java标准的uri,上节已提到过】 部分进行解码的字符集是在 connector 的 <Connector URIEncoding=”UTF-8”/> 中定义的,如果没有定义,那么将以默认编码 ISO-8859-1 解析。所以如果有中文 URL 时最好把 URIEncoding 设置成 UTF-8 编码。

QueryString 的解码字符集要么是 Header 中 ContentType 中定义的 Charset 要么就是默认的 ISO-8859-1,要使用 ContentType 中定义的编码就要设置 connector 的 <Connector URIEncoding=”UTF-8” useBodyEncodingForURI=”true”/> 中的 useBodyEncodingForURI 设置为 true。这个配置项的名字有点让人产生混淆,它并不是对整个 URI 都采用 BodyEncoding 进行解码而仅仅是对 QueryString 使用 BodyEncoding 解码,这一点还要特别注意。

关于post表单提交的数据,存放在请求体中,至于如何解码,需要以后探讨。


六、请求头编码与网页内置编码优先级

设置网页编码的方法有:

1.利用php header()函数声明,这个header()函数的作用是把括号里面的信息发到http标头。

header(“Content-type: text/html; charset=xxx”); 

2.利用HTML <meta >进行声明,HTML <meta >这个标签的作用是声明客户端的浏览器用什么字符集编码显示该页面

<META http-equiv=”content-type” content=”text/html; charset=xxx”>

3.服务器在response 网页时,也可以 设置 服务器默认响应 编码。就相当于给每个文件都 加了一行header(“content-type:text/html; charset=xxx”)。如果网页里有header(“content-type:text/html; charset=xxx”),就把默认的字符集改为你设置的字符集,所以这个函数永远有用。

所以,网页编码的优先级是:

header(“content-type:text/html; charset=xxx”) 
AddDefaultCharset xxx 
<META http-equiv=”content-type” content=”text/html; charset=xxx”> 

如果你是web程序员,给你的每个页面都加个header(“content-type:text/html; charset=xxx”),保证它在任何服务器都能正确显示,可移植性强。 

1,2点 在服务器和浏览器上都可以使用,第3点只能在服务器上使用。

七、其它需要编码的地方

除了 URL 和参数编码问题外,在服务端还有很多地方可能存在编码,如可能需要读取 xml、velocity 模版引擎、JSP 或者从数据库读取数据等。

xml 文件可以通过设置头来制定编码格式

<?xml version="1.0" encoding="UTF-8"?>

Velocity 模版设置编码格式:

services.VelocityService.input.encoding=UTF-8

JSP 设置编码格式:

<%@page contentType="text/html; charset=UTF-8"%>

访问数据库都是通过客户端 JDBC 驱动来完成,用 JDBC 来存取数据要和数据的内置编码保持一致,可以通过设置 JDBC URL 来制定如 MySQL:url=”jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK”。


PHP程序在查询数据库之前,首先执行 mysql_query(“SET NAMES xxxx”);其中 xxxx 是你网页的编码(charset=xxxx),如果网页中 charset=utf8,则 xxxx=utf8,如果网页中 charset=gb2312,则xxxx=gb2312,几乎所有WEB程序,都有一段连接数据库的公共代码,放在一个文件里,在这文件里,加入 mysql_query(“set names”)就可以了。

SET NAMES 显示客户端发送的 SQL 语句中使用什么字符集。因此,SET NAMES ‘utf-8’语句告诉服务器“将来从这个客户端传来的信息采用字符集utf-8”。它还为服务器发送回客户端的结果指定了字符集。(例如,如果你使用一 个SELECT语句,它表示列值使用了什么字符集。)


八、乱码出现的常见原因

在了解了 Java Web 中可能需要编码的地方后,下面看一下,当我们碰到一些乱码时,应该怎么处理这些问题?出现乱码问题唯一的原因都是在 char 到 byte 或 byte 到 char 转换中编码和解码的字符集不一致导致的,由于往往一次操作涉及到多次编解码,所以出现乱码时很难查找到底是哪个环节出现了问题,下面就几种常见的现象进行分析。

1、中文变成了看不懂的字符

例如,字符串“淘!我喜欢!”变成了“Ì Ô £ ¡Î Ò Ï²»¶ £ ¡”编码过程如下图所示

字符串在解码时所用的字符集与编码字符集不一致导致汉字变成了看不懂的乱码,而且是一个汉字字符变成两个乱码字符。

2、一个汉字变成一个问号

例如,字符串“淘!我喜欢!”变成了“??????”编码过程如下图所示

将中文和中文符号经过不支持中文的 ISO-8859-1 编码后,所有字符变成了“?”,这是因为用 ISO-8859-1 进行编解码时遇到不在码值范围内的字符时统一用 3f 表示,这也就是通常所说的“黑洞”,所有 ISO-8859-1 不认识的字符都变成了“?”。

3、一个汉字变成两个问号

例如,字符串“淘!我喜欢!”变成了“????????????”编码过程如下图所示

这种情况比较复杂,中文经过多次编码,但是其中有一次编码或者解码不对仍然会出现中文字符变成“?”现象,出现这种情况要仔细查看中间的编码环节,找出出现编码错误的地方。

4、网页中head标签设置的编码和保存网页的编码不同

我用  gb2312 编码 保存了一个网页【大白话,里面的中文都是gb2312编码】。然后我在该网页中,<META http-equiv=”content-type” content=”text/html; charset=xxx”>设置了utf-8编码。  于是出现乱码了。

5、一种不正常的正确编码

还有一种情况是在我们通过 request.getParameter 获取参数值时,当我们直接调用

String value = request.getParameter(name);

会出现乱码,但是如果用下面的方式

String value = String(request.getParameter(name).getBytes("
ISO-8859-1"), "GBK");

解析时取得的 value 会是正确的汉字字符,这种情况是怎么造成的呢?

看下如所示:

这种情况是这样的,ISO-8859-1 字符集的编码范围是 0000-00FF,正好和一个字节的编码范围相对应。这种特性保证了使用 ISO-8859-1 进行编码和解码可以保持编码数值“不变”。虽然中文字符在经过网络传输时,被错误地“拆”成了两个欧洲字符,但由于输出时也是用 ISO-8859-1,结果被“拆”开的中文字的两半又被合并在一起,从而又刚好组成了一个正确的汉字。虽然最终能取得正确的汉字,但是还是不建议用这种不正常的方式取得参数值,因为这中间增加了一次额外的编码与解码,这种情况出现乱码时因为 Tomcat 的配置文件中 useBodyEncodingForURI 配置项没有设置为”true”,从而造成第一次解析式用 ISO-8859-1 来解析才造成乱码的。


九、http编码优先级及编码别名的宽容性

1、编码优先级问题

根据 HTML4.01 规范中的描述,服务端应该提供给用户端文档的字符编码(character encoding)信息,最直接的方式为通过 HTTP 协议([RFC2616], 3.4 及 14.17) “Content-Type” 头字段的 “charset” 将文档的字符编码告诉用户端。例如以下 HTTP 头声明了字符编码为 ISO-8859-1:

Content-Type: text/html; charset=ISO-8859-1

处于某种情况无法访问服务器时,HTML 文档可以包含有关文档的字符编码的明确信息,META 元素可以用来为用户端提供这些信息。例如指定当前文档的字符编码为 ISO-8859-1,文档中应包含如下 META 声明:

<META http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

当 HTTP 协议与 META 元素均没有提供有关一个文档的字符编码信息时,HTML 还为一些元素提供了 charset 属性。结合这些机制,作者可以在很大程度上提高当用户获取资源时用户端识别字符编码的机会。

针对如何确定一个文档的字符编码,用户代码必须遵守下面的优先级顺序(优先级由高至低):

(1)HTTP “Content-Type” 字段中的 “charset” 参数。
(2)META 声明中 “http-equiv” 为 “Content-Type” 对应的值中的 “charset” 的值。
(3)元素的 charset 属性。

关于 字符编码 的详细信息,请参考 HTML4.01 规范 5.2 Character encodings 以及 W3C Internationalization 关于 Character encodings 中的内容。

2、编码别名宽容性问题

补充知识:做个测试,我们就能明白各浏览器对页面的默认字符编码不尽相同。
当页面没有设置任何字符编码信息或者浏览器无法识别 HTTP 头字段以及 META 元素中所声明的字符编码信息时,所有浏览器均会以各自在当前语言版本下的默认字符编码显示页面。

编码别名宽容性举例:

<?php
  header("Content-Type: text/html; charset=maccyrillic");
?>
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=b.i.g+5"/>
</head>
<body style="font:24px Tahoma;">
字符編碼 --- 
<script>
  document.write((document.charset || document.characterSet).toUpperCase());
</script>
</body>
</html>

上面的动态页面自身的编码为 BIG5,HTTP “Content-Type” 头字段设置了字符编码为 maccyrillic,页面中的 META 元素设置了字符编码为 b.i.g+5。

各浏览器中运行效果如下:

IE6 IE7 IE8 Firefox Chrome Safari Opera
才絪絏 — GB2312 ¶r≤≈љsљX — X-MAC-CYRILLIC 字符編碼 — BIG5

(1)     在 IE6 IE7 IE8 Firefox 中,浏览器无法识别 maccyrillic 这种字符编码别名,所以寻找更低优先级的 META 元素声明的字符编码,但发现也无法识别 b.i.g+5 这种字符编码别名,继而采用了当前语言版本的默认编码 GB2312,与页面自身的字符编码 BIG5 不相符,导致页面中的文字显示异常。
(2)     在 Chrome Safari 中,浏览器将 maccyrillic 识别为合法的 X-MAC-CYRILLIC,则不再理会更低优先级的编码设置,页面采用 X-MAC-CYRILLIC 作为字符的编码,与页面自身的 BIG5 编码不符,导致页面中的文字显示异常。
(3)     在 Opera 中,浏览器无法识别 maccyrillic 这种字符编码别名,所以寻找更低优先级的 META 元素声明的字符编码,将 b.i.g+5 这种字符编码别名识别为 BIG5,刚好与页面自身的 BIG5 字符编码相符,所以页面中的文字显示正常。

出现上述现象的原因主要有三点:

(1)      各浏览器的字符编码别名表不尽相同,对同一种编码下的各种别名支持的宽泛程度不一样。像 maccyrillic 这种别名在 Chrome Safari 可以识别为通用的 X-MAC-CYRILLIC1,但其他浏览器则会将其视作错误的字符编码信息处理。
(2)     各浏览器在匹配页面的字符编码与别名表中的字符编码时,匹配的方式不同。Chrome Safari Opera 会将编码类型的字符串做一个转换,过滤了除英文大小写字符、数字字符之外的字符(isASCIIAlphanumeric)。如 ISO8859_5 会以转换后的 ISO88595 与别名表中的 ISO-8859-5 转换后的 ISO88595 做比较,b.i.g+5 也会转换为 big5 与别名表中的做比较,所以浏览器可以正确识别这些设置的字符编码为浏览器内部的别名。
(3)     各浏览器的默认字符编码不同。
注:各浏览器均可以识别 X-MAC-CYRILLIC 这种通用的字符编码别名。


附录:http 抓包工具 和  测试网页代码

1、http抓包工具fiddler2

2、测试网页代码下载

GBK编码的文件:
<!DOCTYPE html>
<html>
<head>
    <title>gb2312网页测试get提交数据方法</title>
    <meta http-equiv="content-type" content="text/html;charset=gb2312">
</head>
<body>
    <center>
        <form action="gbk-response/春节" method="get">
            <p>用户名:<input type="text" name="name"/></p>
            <p>密码:<input type="password" name="pwd"/></p>
            <input type="submit" value="登录">

        </form>
    </center>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
    <title>gb2312网页测试get提交数据方法</title>
    <meta http-equiv="content-type" content="text/html;charset=gb2312">
</head>
<body>
    <center>
        <form action="gbk-response/春节"  method="post">
            <p>用户名:<input type="text" name="name"/></p>
            <p>密码:<input type="password" name="pwd"/></p>
            <input type="submit" value="登录">

        </form>
    </center>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
    <title>gb2312网页测试get提交数据方法</title>
    <meta http-equiv="content-type" content="text/html;charset=gb2312">
</head>
<body>
    <center>
       
        gb2312 响应页
        
    </center>
</body>
</html>
UTF-8编码的文件:
<!DOCTYPE html>
<html>
<head>
    <title>utf-8网页测试get提交数据方法</title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
    <center>
        <form action="utf-8-response/春节" method="get">
            <p>用户名:<input type="text" name="name"/></p>
            <p>密码:<input type="password" name="pwd"/></p>
            <input type="submit" value="登录">

        </form>
    </center>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
    <title>utf-8网页测试post提交数据方法</title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
    <center>
        <form action="utf-8-response/春节" method="post">
            <p>用户名:<input type="text" name="name"/></p>
            <p>密码:<input type="password" name="pwd"/></p>
            <input type="submit" value="登录">

        </form>
    </center>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
    <title>utf-8网页测试get提交数据方法</title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
    <center>
        utf-8 响应页
    </center>
</body>
</html>

查看 网页的请求和响应,可以用 fiddler2 或者 浏览器开发者工具 –> network –>网页–>headers


参考:

http://www.w3help.org/zh-cn/causes/HR9001
https://blog.csdn.net/xiaokuikey/article/details/50517015
http://aub.iteye.com/blog/763117
https://www.cnblogs.com/JemBai/archive/2010/11/10/1873764.html
http://www.ruanyifeng.com/blog/2010/02/url_encoding.html
http://kaozjlin.iteye.com/blog/1038802
https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html

网络原理-计算机网络详解-因特网与中国网络发展史

计算机网络与Internet发展历史

  • 1957年:苏联发射了人类第一颗人造地球卫星“ Sputnik”
  • 1958年:美国国防部(DoD)组建了高级研究计划局(ARPA)

1961-1972:早期分组交换原理的提出与应用

  • 1961: Kleinrock – 排队论证实分组交换的有效性
  • 1964: Baran – 分组交换应用于军事网络
  • 1967: ARPA(Advanced
    Research ProjectsAgency)提出ARPAnet构想
  • 1969: 第一个ARPAnet 结点运行:美国国防部委托开发ARPANET,进行联网研究
  • 1972:
    • ARPAnet公开演示
    • 第一个主机-主机协议NCP(Network Control Protocol)
    • 第一个e-mail程序
    • ARPAnet拥有15个结点
  • 1972年:招开计算机通信国际会议,称为ICCC(NCP网络协议)

1972-1980:网络互连,大量新型、私有网络的涌现

  • 1970:在夏威夷构建了ALOHAnet卫星网络
  • 1974: Cerf 与 Kahn – 提出网络互连体系结构
  • 1976: Xerox设计了以太网
  • 70’s后期:
    • 私有网络体系结构: DECnet,SNA, XNA
    • 固定长度分组交换 (ATM 先驱)
  • 1975: ARPAnet移交给美国国防部通信局管理
  • 1979: ARPAnet拥有200结点

1980-1990:新型网络协议与网络的激增

  • 1983年1月1日:所有连入ARPANET的主机向TCP/IP转变,即部署TCP/IP
  • 同年ARPANET分成MILNET和NSFNET
  • 1983: 部署TCP/IP
  • 1982: 定义了smtp电子邮件协议
  • 1983: 定义了DNS
  • 1985: 定义了FTP协议
  • 1988: TCP拥塞控制
  • 1990年,NSFNET彻底代替了ARPANET成为Internet的主干

新型国家级网络:CSnet, BITnet, NSFnet,Minitel(法国)

  • 1986: NSFnet初步形成了一个由骨干网、区域网和校园网组成的三级网络
  • 100,000台主机连接公
    共网络

1990, 2000’s: 商业化, Web, 新应用

  • 1990’s早期: ARPAnet退役
  • 1991: NSF解除NSFnet的商业应用限制(1995年退役),由私营企业经营
  • 1992:因特网协会ISOC成立
  • 1990s后期: Web应用
    • 超文本(hypertext) [Bush1945, Nelson 1960’s]
    • HTML, HTTP: Berners-Lee
    • 1994: Mosaic、 Netscape浏览器
    • 1990’s后期:Web开始商业应用
  • 1990’s后期 – 2000’s:
    • 更多极受欢迎的网络应用:即时消息系统(如QQ),P2P文件共享
  • 网络安全引起重视
  • 网络主机约达50000, 网络用户达1亿以上
  • 网络主干链路带宽达到Gbps

2005-今

  • ~7.5亿主机
    • 智能手机和平板电脑
  • 宽带接入的快速部署
  • 无处不在的高速无线接入快速增长
  • 出现在线社交网络:
    • Facebook: 很快拥有10亿用户
  • 服务提供商 (如Google, Microsoft)创建其自己的专用网络
    • 绕开Internet,提供“即时”接入搜索、 email等服务
    • 电子商务、大学、企业等开始在“云”中运行自己的服务(如, Amazon EC2)

中国最早期拨号访问国际互联网

从1986年开始,国内一些科研单位,通过长途电话拨号到欧洲的一些国家,进行联机数据库检索。不久,利用这些国家与Internet的连接,进行E-mail通信。
从1990年开始,国内的北京市计算机应用研究所、中科院高能物理研究所、电子部华北计算所、电子部石家庄第54研究所等科研单位,先后将自己的计算机与CNPAC(X.25)相连接。同时,利用欧洲国家的计算机作为网关,在X.25网与Internet之间进行转接,使得中国的CNPC科技用户可以与Internet用户进行E-mail通信。
1993年3月,中国科学院高能物理研究所(IHEP)为了支持国外科学家使用北京正负电子对撞机做高能物理实验,开通了一条64kbps国际数据信道,连接北京西郊的中科院高能所和美国斯坦福线性加速器中心(SLAC),运行DECnet协议,虽然不能提供完全的Internet功能,但经SAC机器的转接,可以与Internet进行Email通信。

 


一、建设NCFC时代背景

1989年,中国的改革开放进入了第二个十年。

美国在计算机网络领域已经经过了20年的发展,美国国防部高级研究计划局计算机网APAR网络于1968年就开始组建,APAR网络面向软件、硬件和数据库资源共享,采用分层的网络协议、包交换技术和分布式控制架构建设,这些技术思路影响着今天的网络体系结构。随着APAR网络向大学和商业的开放,信息技术和信息产业成为促进经济发展的引擎,美国国家科学基金会大力组织建设了科研网络环境和超级计算环境,使美国保持着信息科技和产业发展的领先地位。

1989年的圣诞假期,在欧洲核物理中心(CERN)工作的蒂姆·伯纳斯-李继续坚持着万维网梦想,为方便研究人员分享及更新讯息,制作完成了第一个万维网浏览器(同时也是编辑器)和第一个网页服务器。这一开发就是WWW技术的原型,当1993年4月30日CERN宣布万维网对任何人免费开放,并不收取任何费用时,WWW真正引爆了互联网的普及。

相比之下,我国电信市场还没有形成,连私人安装电话仍是颇为困难和奢侈的事情,计算机网络也仅有零星建设,没有互联,科研工作就更谈不上实质的数据资源和计算资源的共享。我国科研人员不得不通过传统邮政和邮电服务与国际同行进行交流,无法及时获取国际科技资料和最新成果,信息手段和时效短板严重影响着科技人员的研究视野、研究选题、计算效率和知识更新,严重制约了国家科技发展。在科研教育单位密集的中关村地区建设高性能的科学计算环境和高速计算机网络,并与国际Internet互联的需求十分迫切。

二、面向科研与教育信息化的NCFC项目建设

在信息化时代的大背景和我国科技与教育工作者对建设计算机网络的迫切需求形势下,当时的国家计委将NCFC列入“世界银行贷款重点学科发展项目”,并于1989年8月组织项目招标,确定中国科学院负责承担工程项目的建设任务。1989年10月,中国科学院按照国家计委的要求组建了NCFC管理委员会,时任中国科学院副院长的胡启恒同志担任管委会主任,国家计委、国家自然科学基金委、国家教委、清华大学和北京大学选派有关领导担任管委会成员,于1990年4月完成了工程建设的各项准备工作。

世界银行对NCFC项目内容界定为主干网,不包括大学和研究机构内部的园区网络,其建设目的是在中关村地区建立一个示范性的超级计算中心,并用高速网络将该地区的中国科学院院网、北京大学校园网、清华大学校园网与NCFC互联。使该地区的科技网人员能使用高速网络并通过网络使用超级计算机资源。

中国科学院院网(CASnet)、北京大学校园网(PUnet)和清华大学校园网(TUnet)则分别由三个单位各自出资、各自建设,国家计委对三个园区级网络进行了资金补助。

在世界银行、国家计委的支持下,该建设项目完成了NCFC主干网建设和三个单位园区网络建设,覆盖了中国科学院中关村地区附件的40多个研究所;部署和运行了64亿次浮点计算能力的超级计算机资源;更重要的是,NCFC项目衍生了计划外的重大成果——1994年4月20日,在国家计委的补助资金支持下,一条64K的国际卫星专线从中科院计算机网络中心通过美国Sprint公司连入Internet,首次实现了中国与Internet的全功能连接。从此中国被国际上正式承认为第77个真正拥有全功能Internet的国家。

在当时的历史条件下,NCFC工程解决了若干技术问题和政策问题,为后来的互联网络环境建设奠定了基础。

NCFC工程确定了以TCP/IP协议为主的技术路线。当时我国的计算机,较为常见的网络协议体系有TCP/IP、DECnet和OSI。要把使用不同网络协议体系的计算机互联,必须在网络通信协议上达成共识才能实现计算机间的信息互通。中科院计算机网络中心总体组坚持实用、开放的原则,明确了以TCP/IP协议为主,以OSI为发展方向,兼顾现有的其他协议的技术路线。这一设计思路,为NCFC网络顺利接入Internet做好了水到渠成的技术准备。

NCFC工程首次使用光纤线路建设计算机主干网络,通过网桥将中国科学院院网(CASnet)、北京大学校园网(PUnet)和清华大学校园网(TUnet)进行联网。综合多样性的技术手段和装备解决网络接入问题,对于校园和城域网络分别采用光纤和微波手段解决;对于长途网络采用X.25、卫星通信等通信手段接入;对于个人用户采用拨号方式接入,为用户接入工作提出了技术先进、综合、全面的解决方案。

=========================================================

CASnet 历史

CASnet 是中国科学院的全国性网络建设工程。该工程分为两部分,一部分为分院区域网络工程,另一部分是用远程信道将各分院区域网络及零星分布在其他城市的研究所互联到NFC网络中心的广域网工程。
CASnet的远程连接,又分为两期工程,第一期用X.25信道将全国3个城市连到北京,第二期以高速卫星信道代替X.25信道,并把更多的城市连到北京。1995年底,CASnet完成了将12个分院区域网及其他城市的研究所连到北京的广域网工程,连接了24个城市(包括北京)。作为第一步,早期的连接通过CHINAPAC实现,速率从9.6kbps到64kbps不等。目前,已将各主要分院的地区网络,用高速的卫星信道连到北京。零星分布的研究所仍暂时沿用ChinaPAC信道,不久将也用卫星连到北京。

=========================================================

技术人员很快发现,NCFC工程采用的网桥技术解决了CASnet、PUnet和TUnet之间的光纤联网,但是无法解决网络中广播风暴的影响,路由器的引入和部署变得十分必要和迫切。由于当时巴黎统筹委员会的限制使我国无法进口路由器,因此中科院计算机网络中心组织了技术人员自行开发路由器,并在NCFC主干网和中科院院网中部署应用,为提升网络可靠性发挥出关键作用,从而使NCFC与当时国外先进的网络同样可以高可靠性运行。

三、突破壁垒,实现与国际互联网全功能接入

Internet对当时的中国来说还是一个新生事物,和传统的电信业务有很大的不同。为了取得国家电信部门在国际专线租用方面的批准和支持,NCFC管委会和邮电部进行了多次的沟通,终于取得了国际专线资源支持。

当NCFC启动与国际联网的时候,美国方面处于政治和安全方面的原因,不接纳我国接入Internet。1993年中科院计算机网络中心钱华林等同志赴美与sprint公司商谈落实了与Internet联网方案,并根据公共网络服务要求,制定了NCFC“准用政策”(AUP),在报经国务院邹家华副总理等有关领导同意后,NCFC管委会主任胡启恒同志在参加美国华盛顿举行的中美科技合作联合委员会第六次会议期间,就NCFC与Internet联网问题,与NSF官员进行商谈,美国同意遵守NSFNET(美国科研与教育骨干网)和NCFC“准用政策”的条件下,实现NCFC与美国NSF主干网连接。随后,在1994年3月开通了国际卫星专线,4月中美两侧路由器开通,同时,在中科院计算机网络中心建立了代表Internet中国顶级域名的.cn服务器、邮件服务器、文件服务器等一系列网络服务器。

  

  中国第一台.CN域名服务器

  随后,中科院计算机网络中心进一步完成了在InterNIC的注册,建立了与国际IneterNIC和APNIC规范的业务联系,并于1994年10月建成了NIC(网络信息中心)和NOC(网络运行中心),对国内外用户服务。自此,中国互联网系统地、整体地展现于世界。

四、NCFC建设的后续演进和发展

NCFC项目圆满完成了网络建设目标和超级计算机建设目标,并实现了与Internet联网。

中国科学院在NCFC和CASnet的基础上,以服务科研信息化为定位和发展方向,建设中国科技网(CSTNET)。经过不断的建设,中国科技网已成为承载国家超级计算资源、科学数据资源、服务国家科技创新的科研信息化技术设施,取得了令人瞩目的应用成果和社会效益。中国科技网具有向全国提供IPv4和IPv6双栈接入的7×24服务能力,提供电子邮件、视频会议、科研协同环境、网络管理、网络安全等丰富的互联网服务产品,在我国嫦娥工程、国际联合天文观测、高能物理海量数据传输、大型科研装置数据传输、野外台站联网观测和应急救灾等方面提供高性能网络支撑和技术支持。

=======================================================

CSTNet历史

CSTNet 是以中国科学院的NCFC及CASnet为基础,连接了中国科学院以外的一批中国科技单位而构成的网络。目前接入CSTNet的单位有农业、林业、医学、电力、地震、气象、铁道、电子、航空航天、环境保护等近30个科研部门及国家自然科学基金委、国家专利局等科技管理部门。目前,CSTNet有2Mbps的信道连到美国,64Kbps的信道连到法国,64Kbps的信道连到日本。【很早之前的历史资料了】
CSTNet为非盈利、公益性的网络,主要为科技用户、科技管理部门及与科技有关的政府部门服务。

=======================================================

由NCFC发展起来的中国互联网络信息中心(CNNIC)行使着国家互联网络信息中心的职责。经过长期的建设和开拓,CNNIC已经成为国家网络基础资源的运行管理和服务机构、国家网络基础资源的技术研发和安全中心、互联网发展研究力量、互联网开放合作和技术交流平台,促进科研成果转化和孵化,服务中国互联网事业发展。

更重要的是,NCFC项目的建成吹响了中国互联网时代的号角。NCFC项目1998年获得中国科学院科技进步特等奖;国家统计局将“国家计算与网络设施(NCFC)实现国内国际联网”列为1994年度国家重大科技成就之一;人民日报将“中国国家计算与网络设施(NCFC)实现国内国际联网成功”列为1994年中国十大科技新闻之一。作为一个示范性网络,NCFC建设在中国掀起了一个发展Internet的热潮,国内先后建设成了中国科技网CSTNET、中国公用计算机互联网CHINANET、中国教育和科研计算机网CERNET和中国金桥信息网CHINAGBN,也就是早期的中国四大互联网络。

今天,生活在互联网时代的我们重新梳理20年前的历史,不仅要向当时的建设者们致以崇高的敬意,更要有同样的勇气和智慧迎接互联网时代的新挑战。作为中国互联网发源地的中科院计算机网络信息中心,现今已经承担起了历史赋予的新使命:致力于中国科学院科研信息化和管理信息化的研发、支撑与服务。如果您到中科院计算机网络信息中心来,依然可以看到当初接入Internet的机房,也可看到我国第一台路由器、第一台CN域名服务器。面对这些历史物件,再转身看看中科院信息化基础设施机房里成排的服务器、大型的高性能计算机,我们无法不慨叹信息时代发展的“火箭速度”,并且暗自庆幸我们正置身于这个“一切皆有可能”的信息社会。

 

 


参考:

http://www.cas.cn/kxcb/kpwz/201404/t20140419_4093686.shtml

https://baike.baidu.com/item/CSTNET

网络原理-计算机网络详解-传输层之TCP协议

一、TCP 协议的作用

互联网由TCP/IP协议族构成。TCP 只是其中的一层,有着自己的分工。

(图片说明:TCP 是以太网协议和 IP 协议的上层协议,也是应用层协议的下层协议。)

最底层的以太网协议(Ethernet)规定了电子信号如何组成数据帧(frame),解决了子网内部的点对点通信。

(图片说明:以太网协议解决了局域网的点对点通信。)

但是,以太网协议不能解决多个局域网如何互通,这由 IP 协议解决。

(图片说明:IP 协议可以连接多个局域网。)

IP 协议定义了一套自己的地址规则,称为 IP 地址。它实现了路由功能,允许某个局域网的 A 主机,向另一个局域网的 B 主机发送消息。

(图片说明:路由器就是基于 IP 协议。局域网之间要靠路由器连接。)

路由的原理很简单。市场上所有的路由器,背后都有很多网口,要接入多根网线。路由器内部有一张路由表,规定了 A 段 IP 地址走出口一,B 段地址走出口二,……通过这套”指路牌”,实现了数据包的转发。

(图片说明:本机的路由表注明了不同 IP 目的地的数据包,要发送到哪一个网口(interface)。)

IP 协议只是一个地址协议,并不保证数据包的完整。如果路由器丢包(比如缓存满了,新进来的数据包就会丢失),就需要发现丢了哪一个包,以及如何重新发送这个包。这就要依靠 TCP 协议。

简单说,TCP 协议的作用是,保证数据通信的完整性和可靠性,防止丢包。

二、TCP 报文段的大小

MTU最大传输单元,这个最大传输单元实际上和链路层协议有着密切的关系,EthernetII帧的结构DMAC+SMAC+Type+Data+CRC由于以太网传输电气方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes,对于小于或者大于这个限制的以太网帧我们都可以视之为错误的数据帧,一般的以太网转发设备会丢弃这些数据帧。

由于以太网EthernetII最大的数据帧是1518Bytes这样,刨去以太网帧的帧头(DMAC目的MAC地址48bit=6Bytes+SMAC源MAC地址48bit=6Bytes+Type域2bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。

以太网数据包(packet)由于环境限制,最大值是固定的,最初协议是1518字节,后来新协议是1522字节。但是MTU 仍是 1500Bytes。

为了允许一些使用以太II版本的数据报和一些使用802.3封装的最初版本的数据包能够在同一个以太网段使用,以太类型值必须大于等于1536(0x0600)。这个值比802.3数据包的最大长度1500byte (0x05DC)要更大。因此如果这个字段的值大于等于1536,则这个帧是以太II帧,而那个字段是类型字段。否则(小于1500而大于46字节),他是一个IEEE 802.3帧,而那个字段是长度字段。1500~1536(不包含)的数值未定义。因为网络环境 MTU选择是1500【即以太网帧最大负载是1500字节】,所以我们上网用的以太网帧 应该是 802.3帧。

来自维基百科

802.3 以太网帧结构
前导码 帧开始符 MAC 目标地址 MAC 源地址 802.1Q 标签 (可选) 以太类型 负载 冗余校验 帧间距
10101010 7个octet 10101011 1个octet 6 octets 6 octets (4 octets) 2 octets 46–1500 octets 4 octets 12 octets
64–1522 octets
72–1530 octets
84–1542 octets

IP 数据包在以太网数据包的负载里面,它也有自己的头信息,最少需要20字节,所以 IP 数据包的负载最多为1480字节。

(图片说明:IP 数据包在以太网数据包里面,TCP 报文段在 IP 数据包里面。)

TCP 报文段在 IP 数据包的负载里面。它的头信息最少也需要20字节,因此 TCP 报文段的最大负载是 1480 – 20 = 1460 字节。由于 IP 和 TCP 协议往往有额外的头信息,所以 TCP 负载实际为1400字节左右。

因此,一条1500字节的信息需要两个 TCP报文段。HTTP/2 协议的一大改进, 就是压缩 HTTP 协议的头信息,使得一个 HTTP 请求可以放在一个 TCP 报文段里面,而不是分成多个,这样就提高了速度。

(图片说明:以太网数据帧的负载是1500字节,TCP 报文段的负载在1400字节左右。)

三、TCP报文段的格式

图释:

抓包过后才发现,ip数据包首部没有添加可选项,就直接装载 tcp 数据了。

各个段位说明:

  • 源端口和目的端口:  各占 2 字节.端口是传输层与应用层的服务接口.传输层的复用和分用功能都要通过端口才能实现
  • 序号:  占 4 字节。用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
  • 确认号:  占 4 字节,期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。
  • 数据偏移/首部长度:  占 4 位,指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。“数据偏移”的单位是 32 位字(以 4 字节为计算单位)
  • 保留:  占 6 位,保留为今后使用,但目前应置为 0
  • 紧急URG:  当 URG=1 时,表明紧急指针字段有效.它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)
  • 确认ACK:  只有当 ACK=1 时确认号字段才有效。当 ACK=0 时,确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
  • PSH(PuSH):  接收 TCP 收到 PSH = 1 的报文段,就尽快地交付接收应用进程,而不再等到整个缓存都填满了后再向上交付
  • RST (ReSeT):  当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接
  • 同步 SYN:  在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。
  • 终止 FIN:  用来释放一个连接。FIN=1 表明此报文段的发送端的数据已发送完毕,并要求释放运输连接
  • 窗口:      窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。
  • 检验和:  占 2 字节.检验和字段检验的范围包括首部和数据这两部分.在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部
  • 紧急指针:  占 16 位,指出在本报文段中紧急数据共有多少个字节(紧急数据放在本报文段数据的最前面)
  • 选项:  长度可变.TCP 最初只规定了一种选项,即最大报文段长度 MSS.MSS 告诉对方 TCP:“我的缓存所能接收的报文段的数据字段的最大长度是 MSS 个字节.” [MSS(Maximum Segment Size)是 TCP 报文段中的数据字段的最大长度.数据字段加上 TCP 首部才等于整个的 TCP 报文段]
  • 填充:  这是为了使整个首部长度是 4 字节的整数倍
  • 其他选项:
    • 窗口扩大:  占 3 字节,其中有一个字节表示移位值 S.新的窗口值等于TCP 首部中的窗口位数增大到(16 + S),相当于把窗口值向左移动 S 位后获得实际的窗口大小
    • 时间戳:  占10 字节,其中最主要的字段时间戳值字段(4字节)和时间戳回送回答字段(4字节)
    • 选择确认:  接收方收到了和前面的字节流不连续的两2字节.如果这些字节的序号都在接收窗口之内,那么接收方就先收下这些数据,但要把这些信息准确地告诉发送方,使发送方不要再重复发送这些已收到的数据

四、TCP报文段特点

数据单位

TCP 传送的数据单位协议是 TCP 报文段(segment)

特点

TCP 是面向连接的传输层协议
每一条 TCP 连接只能有两个端点(endpoint),每一条 TCP 连接只能是点对点的(一对一)
TCP 提供可靠交付的服务
TCP 提供全双工通信
面向字节流

注意

TCP 对应用进程一次把多长的报文发送到TCP 的缓存中是不关心的
TCP 根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应包含多少个字节(UDP 发送的报文长度是应用进程给出的)
TCP 可把太长的数据块划分短一些再传送.TCP 也可等待积累有足够多的字节后再构成报文段发送出去
每一条 TCP 连接有两个端点
TCP 连接的端点不是主机,不是主机的IP 地址,不是应用进程,也不是传输层的协议端口.TCP 连接的端点叫做套接字(socket)或插口


五、TCP报文段的编号(SEQ)

一个报文段1400字节,那么一次性发送大量数据,就必须分成多个报文段(segment)。比如,一个 10MB 的文件,需要发送7100多个包。

发送的时候,TCP 协议为每个报文段编号(sequence number,简称 SEQ),以便接收的一方按照顺序还原。万一发生丢分段,也可以知道丢失的是哪一个报文段。

第一个报文段的编号是一个随机数。为了便于理解,这里就把它称为1号报文段。假定这个报文段的负载长度是100字节,那么可以推算出下一个报文段的编号应该是101。这就是说,每个报文段都可以得到两个编号:自身的编号,以及下一个报文段的编号。接收方由此知道,应该按照什么顺序将它们还原成原始文件。

(图片说明:当前分段的编号是45943,下一个数据分段的编号是46183,由此可知,这个分段的负载是240字节。)

六、TCP 报文段的组装

收到 TCP 报文段以后,组装还原是操作系统完成的。应用程序不会直接处理 TCP 报文段。

对于应用程序来说,不用关心数据通信的细节。除非线路异常,收到的总是完整的数据。应用程序需要的数据放在 TCP 报文段里面,有自己的格式(比如 HTTP 协议)。

TCP 并没有提供任何机制,表示原始文件的大小,这由应用层的协议来规定。比如,HTTP 协议就有一个头信息Content-Length,表示信息体的大小。对于操作系统来说,就是持续地接收 TCP 报文段,将它们按照顺序组装好,一个分段都不少。

操作系统不会去处理 TCP 报文段里面的数据。一旦组装好 TCP 报文段,就把它们转交给应用程序。TCP 报文段里面有一个端口(port)参数,就是用来指定转交给监听该端口的应用程序。

(图片说明:系统根据 TCP 报文段里面的端口,将组装好的数据转交给相应的应用程序。上图中,21端口是 FTP 服务器,25端口是 SMTP 服务,80端口是 Web 服务器。)

应用程序收到组装好的原始数据,以浏览器为例,就会根据 HTTP 协议的Content-Length字段正确读出一段段的数据。这也意味着,一次 TCP 通信可以包括多个 HTTP 通信。


七、建立连接和断开连接过程

报文段的发送时机

TCP 维持一个变量,它等于最大报文段长度 MSS.只要缓存中存放的数据达到 MSS 字节时,就组装成一个 TCP 报文段发送出去
由发送方的应用进程指明要求发送报文段,即 TCP 支持的推送(push)操作
发送方的一个计时器期限到了,这时就把当前已有的缓存数据装入报文段(但长度不能超过 MSS)发送出去

发送TCP请求客户端

三次连接四次挥手

三个阶段:

  • 连接建立:
    • 图释:

    • 步骤:
      • A 的 TCP 向 B 发出连接请求报文段,其首部中的同步位 SYN = 1,并选择序号 seq = x,表明传送数据时的第一个数据字节的序号是 x
      • B 的 TCP 收到连接请求报文段后,如同意,则发回确认(B 在确认报文段中应使 SYN = 1,使 ACK = 1,其确认号ack = x﹢1,自己选择的序号 seq = y)
      • A 收到此报文段后向 B 给出确认,其 ACK = 1,确认号 ack = y﹢1(A 的 TCP 通知上层应用进程,连接已经建立,B 的 TCP 收到主机 A 的确认后,也通知其上层应用进程:TCP 连接已经建立)
  • 数据传送
  • 连接释放:
    • 图释:

    • 步骤:
      • 数据传输结束后,通信的双方都可释放连接.现在 A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据,主动关闭 TCP 连接(A 把连接释放报文段首部的 FIN = 1,其序号seq = u,等待 B 的确认)
      • B 发出确认,确认号 ack = u+1,而这个报文段自己的序号 seq = v(TCP 服务器进程通知高层应用进程.从 A 到 B 这个方向的连接就释放了,TCP 连接处于半关闭状态.B 若发送数据,A 仍要接收)
      • 若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接
      • A 收到连接释放报文段后,必须发出确认,在确认报文段中 ACK = 1,确认号 ack=w﹢1,自己的序号 seq = u + 1
    • 注意:

TCP 连接必须经过时间 2MSL 后才真正释放掉(2MSL 的时间的用意 — 为了保证 A 发送的最后一个 ACK 报文段能够到达 B.防止 “已失效的连接请求报文段”出现在本连接中.A 在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所有报文段,都从网络中消失.这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段)

    • 发现丢失确认时候的处理:

三个问题:

  • 要使每一方能够确知对方的存在
  • 要允许双方协商一些参数(如最大报文段长度,最大窗口大小,服务质量等)
  • 能够对运输实体资源(如缓存大小,连接表中的项目等)进行分配

四次挥手 :

A:我这边传完了,你那边收接收完了没?B:我这边接收完了。A就不传输信息了。

B:我这边传完了,你那边收接收完了没?A:我这边接收完了。B就不传输信息了。


七、滑动窗口(发送窗口-接收窗口-拥塞窗口)

滑动窗口

图释:

特点:

  • 以字节为单位的滑动窗口
  • A 的发送窗口并不总是和 B 的接收窗口一样大(因为有一定的时间滞后)

要求:

  • TCP 标准没有规定对不按序到达的数据应如何处理.通常是先临时存放在接收窗口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用进程
  • TCP 要求接收方必须有累积确认的功能,这样可以减小传输开销

具体实现: 


七、拥塞避免

服务器发送数据包,当然越快越好,最好一次性全发出去。但是,发得太快,就有可能丢包。带宽小、路由器过热、缓存溢出等许多因素都会导致丢包。线路不好的话,发得越快,丢得越多。

最理想的状态是,在线路允许的情况下,达到最高速率。但是我们怎么知道,对方线路的理想速率是多少呢?

1、慢开始算法

TCP 协议为了做到效率与可靠性的统一,设计了一个慢启动(slow start)机制。开始的时候,发送得较慢,然后根据丢报文段的情况,调整速率:如果不丢报文段,就加快发送速度;如果丢报文段,就降低发送速度。

Linux 内核里面设定了(常量TCP_INIT_CWND),刚开始通信的时候,发送方一次性发送10个报文段,即”发送窗口”的大小为10。然后停下来,等待接收方的确认,再继续发送。

默认情况下,接收方每收到两个 TCP 报文段,就要发送一个确认消息。”确认”的英语是 acknowledgement,所以这个确认消息就简称 ACK。

ACK 携带两个信息。

  • 期待要收到下一个数据包的编号
  • 接收方的接收窗口的剩余容量

发送方有了这两个信息,再加上自己已经发出的报文段的最新编号,就会推测出接收方大概的接收速度,从而降低或增加发送速率。这被称为”发送窗口”,这个窗口的大小是可变的。

(图片说明:每个 ACK 都带有下一个报文段的编号,以及接收窗口的剩余容量。双方都会发送 ACK。)

注意,由于 TCP 通信是双向的,所以双方都需要发送 ACK。两方的窗口大小,很可能是不一样的。而且 ACK 只是很简单的几个字段,通常与数据合并在一个报文段里面发送。

(图片说明:上图一共4次通信。第一次通信,A 主机发给B 主机的报文段编号是1,长度是100字节,因此第二次通信 B 主机的 ACK 编号是 1 + 100 = 101,第三次通信 A 主机的报文段编号也是 101。同理,第二次通信 B 主机发给 A 主机的报文段编号是1,长度是200字节,因此第三次通信 A 主机的 ACK 是201,第四次通信 B 主机的报文段编号也是201。)

即使对于带宽很大、线路很好的连接,TCP 也总是从10个报文段开始慢慢试,过了一段时间以后,才达到最高的传输速率。这就是 TCP 的慢启动。


慢开始算法:

  • 在主机刚刚开始发送报文段时可先设置拥塞窗口 cwnd = 1,即设置为一个最大报文段 MSS 的数值
  • 在每收到一个对新的报文段的确认后,将拥塞窗口加 1,即增加一个 MSS 的数值
  • 使用慢开始算法后,每经过一个传输轮次(往返时间 RTT),拥塞窗口 cwnd 就加倍

2、拥塞避免算法:

拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加 1,使拥塞窗口 cwnd 按线性规律缓慢增长

3、慢开始门限 ssthresh 的用法:

  • 当 cwnd < ssthresh 时,使用慢开始算法
  • 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法
  • 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞避免算法

4、网络出现拥塞时(其根据就是没有按时收到确认):

  • 就要把慢开始门限 ssthresh 设置为出现拥塞时的发送方窗口值的一半(但不能小于2)
  • 然后把拥塞窗口 cwnd 重新设置为 1.执行慢开始算法

八、拥塞处理

拥塞窗口:

含义:

拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化.发送方让自己的发送窗口等于拥塞窗口.如再考虑到接收方的接收能力,则发送窗口还可能小于拥塞窗口

发送方控制拥塞窗口的原则:

只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去.但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数

乘法减小:

是指不论在慢开始阶段还是拥塞避免阶段,只要出现一次超时(即出现一次网络拥塞),就把慢开始门限值 ssthresh 设置为当前的拥塞窗口值乘以 0.5

加法增大:

是指执行拥塞避免算法后,在收到对所有报文段的确认后(即经过一个往返时间),就把拥塞窗口 cwnd增加一个 MSS 大小,使拥塞窗口缓慢增大,以防止网络过早出现拥塞

快重传:

每一个TCP报文段都带有下一个报文段的编号。如果下一个报文段没有收到,那么 ACK 的编号就不会发生变化。

举例来说,现在收到了4号报文段,但是没有收到5号报文段。ACK 就会记录,期待收到5号报文段。过了一段时间,5号报文段收到了,那么下一轮 ACK 会更新编号。如果5号报文段还是没收到,但是收到了6号报文段或7号报文段,那么 ACK 里面的编号不会变化,总是显示5号报文段。这会导致大量重复内容的 ACK。

如果发送方发现收到三个连续的重复 ACK,或者超时了还没有收到任何 ACK,就会确认丢失报文段,即5号报文段遗失了,从而再次发送这个报文段。通过这种机制,TCP 保证了不会有报文段丢失。

(图片说明:Host B 没有收到100号报文段,会连续发出相同的 ACK,触发 Host A 重发100号报文段。)

快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认.这样做可以让发送方及早知道有报文段没有到达接收方,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段

快恢复:

当发送端收到连续三个重复的确认时,就执行“乘法减小”算法,把慢开始门限 ssthresh 减半.但接下去不执行慢开始算法

发送窗口的上限值:

发送方的发送窗口的上限值应当取为接收方窗口 rwnd 和拥塞窗口 cwnd 这两个变量中较小的一个,即应按以下公式确定:
发送窗口的上限值 Min [rwnd, cwnd]

    • 当 rwnd < cwnd 时,是接收方的接收能力限制发送窗口的最大值
    • 当 cwnd < rwnd 时,则是网络的拥塞限制发送窗口的最大值

九、报文段的遗失处理-自动重传

TCP 协议可以保证数据通信的完整性,这是怎么做到的?

自动重传请求ARQ

定义:

可靠传输协议常称为自动重传请求ARQ (Automatic Repeat reQuest)

累积确认:

  • 定义:  接收方一般采用累积确认的方式.即不必对收到的分组逐个发送确认,而是对按序到达的最后一个分组发送确认,这样就表示:到这个分组为止的所有分组都已正确收到了
  • 优点:  容易实现,即使确认丢失也不必重传
  • 缺点:  不能向发送方反映出接收方已经正确收到的所有分组的信息

Go-back-N(回退N):

如果发送方发送了前 5 个分组,而中间的第 3 个分组丢失了.这时接收方只能对前两个分组发出确认.发送方无法知道后面三个分组的下落,而只好把后面的三个分组都再重传一次

具体实现

说明:

  • TCP 连接的每一端都必须设有两个窗口 一个发送窗口和一个接收窗口
  • TCP 可靠传输机制用字节的序号进行控制.TCP 所有的确认都是基于序号而不是基于报文段
  • TCP 两端的四个窗口经常处于动态变化之中
  • TCP连接的往返时间 RTT 也不是固定不变的.需要使用特定的算法估算较为合理的重传时间

图释:

确认丢失和确认迟到

超时重传时间选择

具体实现:

TCP 每发送一个报文段,就对这个报文段设置一次计时器.只要计时器设置的重传时间到但还没有收到确认,就要重传这一报文段

加权平均往返时间:

做法:

TCP 保留了 RTT 的一个加权平均往返时间 RTTS(这又称为平滑的往返时间),第一次测量到 RTT 样本时,RTTS 值就取为所测量到的 RTT 样本值.以后每测量到一个新的 RTT 样本,就按下式重新计算一次 RTTS:

公式:

新的 RTTS = ( 1 – α)×(旧的 RTTS)+α(新的 RTT 样本)

说明:

式中,0 ≤ α< 1.若α很接近于零,表示 RTT 值更新较慢若选择 α 接近于1,则表示 RTT 值更新较快
RFC 2988 推荐的 α 值为 1/8,即 0.125

超时重传时间RTO:

RTO 应略大于上面得出的加权平均往返时间 RTTS.
RFC 2988 建议使用下式计算 RTO:

RTO=RTTS + 4×RTTD

RTTD 是 RTT 的偏差的加权平均值
RFC 2988 建议这样计算 RTTD.第一次测量时,RTTD 值取为测量到的 RTT 样本值的一半.在以后的测量中,则使用下式计算加权平均的 RTTD:

新的 RTTD = (1-β)×(旧的RTTD)+β×|RTTS﹣新的 RTT 样本|

β是个小于 1 的系数,其推荐值是 1/4,即 0.25
在计算平均往返时间 RTT 时,只要报文段重传了,就不采用其往返时间样本

修正的Karn算法:

报文段每重传一次,就把 RTO 增大一些:

新的 RTO= γ×(旧的 RTO)

系数γ 的典型值是 2
当不再发生报文段的重传时,才根据报文段的往返时延更新平均往返时延 RTT 和超时重传时间 RTO 的数值

持续计时器

  • TCP 为每一个连接设有一个持续计时器
  • 只要 TCP 连接的一方收到对方的零窗口通知,就启动持续计时器
  • 若持续计时器设置的时间到期,就发送一个零窗口探测报文段(仅携带 1 字节的数据),而对方就在确认这个探测报文段时给出了现在的窗口值
  • 若窗口仍然是零,则收到这个报文段的一方就重新设置持续计时器
  • 若窗口不是零,则死锁的僵局就可以打破了

九、缓存控制

发送缓存

发送缓存用来暂时存放:

  • 发送应用程序传送给发送方 TCP 准备发送的数据
  • TCP 已发送出但尚未收到确认的数据

图释:

接收缓存

接收缓存用来暂时存放:

  • 按序到达的、但尚未被接收应用程序读取的数据;
  • 不按序到达的数据

 图释:

 

 


参考:

http://www.cnblogs.com/kzang/articles/2582957.html

http://www.ruanyifeng.com/blog/2017/06/tcp-protocol.html

https://blog.csdn.net/u014222687/article/details/55002177

网络原理-计算机网络详解-传输层概述

我们这里只讲TCP/IP 网络模型的传输层协议,传输层协议有TCP协议和UDP协议。

一、传输层

传输层的作用

TCP提供可靠的通信传输,而UDP则常被用于让广播和细节控制交给应用的通信传输。

传输层定义

IP首部有一个协议字段,用来标识网络层(IP)的上一层所采用的是哪一种传输层协议。根据这个字段的协议号,就可以识别IP传输的数据部分究竟是TCP的内容还是UDP的内容。

同样,传输层的TCP和UDP,为了识别自己所传输的数据部分究竟应该发给哪个应用,也设定了这样一个编号。

在TCP/IP通信中需要指定“应用程序”。而传输层必须指出这个具体的程序,为了实现这一功能,使用端口号这样一种识别码。根据端口号可以识别在传输层上一层的应用层中所要进行处理的具体程序。

通信处理

服务端程序在UNIX系统当中叫做守护进程。例如HTTP的服务端程序是httpd(HTTP守护进程),而ssh的服务端程序是sshd(SSH守护进程)。UNIX中并不需要将这些守护进程逐个启动,而是启动一个可以代表它们接收客户端请求的inetd(互联网守护进程)服务程序即可。它是一种超级守护进程。该超级守护进程收到客户端请求以后会创建(fork)新的进程并转换(exec)为sshd等各个守护进程。

确认一个请求究竟发给的是哪个服务端(守护进程),可以通过所收到数据包的目标端口号轻松识别。当收到TCP的建立连接请求时,如果目标端口为22,则转给sshd,如果是80则转给httd。然后,这些守护进程会继续对该连接上的通信传输进行处理。

传输协议TCP、UDP通过接收数据中的目标端口识别目标处理程序。以下图为例,传输协议的数据将被传递给HTTP、TELNET以及FTP等应用层协议。

HTTP连接请求:

通过IP地址、端口号、协议号进行通信识别

TCP/IP或UDP/IP通信中通常采用5个信息来识别一个通信。它们是“源IP地址”、“目标IP地址”、“协议号”、“源端口号”、“目标端口号”。只要其中某一项不同,则被认为是其他通信。

 

二、SOCKET的用处

Socket是什么呢?
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

大白话:我们编写的应用层协议:http 、ftp、smtp 都是调用socket 接口实现的。

三、TCP与UDP简介

TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂,我们这里只做简单、形象的介绍,你只要做到能够理解这个过程即可。我们来看看这三次对话的简单过程:主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话;主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话;主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据。

UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!
UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。比如,我们经常使用“ping”命令来测试两台主机之间TCP/IP通信是否正常,其实“ping”命令的原理就是向对方主机发送UDP数据包,然后对方主机确认收到数据包,如果数据包是否到达的消息及时反馈回来,那么网络就是通的。例如,在默认状态下,一次“ping”操作发送4个数据包(如图2所示)。大家可以看到,发送的数据包数量是4包,收到的也是4包(因为对方主机收到后会发回一个确认收到的数据包)。这充分说明了UDP协议是面向非连接的协议,没有建立连接的过程。正因为UDP协议没有连接的过程,所以它的通信效果高;但也正因为如此,它的可靠性不如TCP协议高。QQ就使用UDP发消息,因此有时会出现收不到消息的情况。

四、TCP与UDP的区别

TCP与UDP基本区别
1.基于连接与无连接
2.TCP要求系统资源较多,UDP较少;
3.UDP程序结构较简单
4.流模式(TCP)与数据报模式(UDP);
5.TCP保证数据正确性,UDP可能丢包
6.TCP保证数据顺序,UDP不保证

UDP应用场景:
1.面向数据报方式
2.网络数据大多为短消息
3.拥有大量Client
4.对数据安全性无特殊要求
5.网络负担非常重,但对响应速度要求高

具体编程时的区别
1.socket()的参数不同
2.UDP Server不需要调用listen和accept
3.UDP收发数据用sendto/recvfrom函数
4.TCP:地址信息在connect/accept时确定
5.UDP:在sendto/recvfrom函数中每次均 需指定地址信息
6.UDP:shutdown函数无效

编程区别
通常我们在说到网络编程时默认是指TCP编程,即用前面提到的socket函数创建一个socket用于TCP通讯,函数参数我们通常填为SOCK_STREAM。即socket(PF_INET, SOCK_STREAM, 0),这表示建立一个socket用于流式网络通讯。
SOCK_STREAM这种的特点是面向连接的,即每次收发数据之前必须通过connect建立连接,也是双向的,即任何一方都可以收发数据,协议本身提供了一些保障机制保证它是可靠的、有序的,即每个包按照发送的顺序到达接收方。

而SOCK_DGRAM这种是User Datagram Protocol协议的网络通讯,它是无连接的,不可靠的,因为通讯双方发送数据后不知道对方是否已经收到数据,是否正常收到数据。任何一方建立一个socket以后就可以用sendto发送数据,也可以用recvfrom接收数据。根本不关心对方是否存在,是否发送了数据。它的特点是通讯速度比较快。大家都知道TCP是要经过三次握手的,而UDP没有。

基于上述不同,UDP和TCP编程步骤也有些不同,如下:

TCP: 
TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt(); * 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、开启监听,用函数listen();
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
8、关闭监听;

TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;

UDP:
与之对应的UDP编程步骤要简单许多,分别如下:
UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、循环接收数据,用函数recvfrom();
5、关闭网络连接;

UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置对方的IP地址和端口等属性;
5、发送数据,用函数sendto();
6、关闭网络连接;

TCP和UDP是OSI模型中的运输层中的协议。TCP提供可靠的通信传输,而UDP则常被用于让广播和细节控制交给应用的通信传输。

UDP补充:
UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务。并且它是将应用程序发来的数据在收到的那一刻,立刻按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况下,UDP也无法进行流量控制等避免网络拥塞的行为。此外,传输途中如果出现了丢包,UDO也不负责重发。甚至当出现包的到达顺序乱掉时也没有纠正的功能。如果需要这些细节控制,那么不得不交给由采用UDO的应用程序去处理。换句话说,UDP将部分控制转移到应用程序去处理,自己却只提供作为传输层协议的最基本功能。UDP有点类似于用户说什么听什么的机制,但是需要用户充分考虑好上层协议类型并制作相应的应用程序。

TCP补充:
TCP充分实现了数据传输时各种控制功能,可以进行丢包的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在UDP中都没有。此外,TCP作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。TCP通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输。

TCP与UDP区别总结:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保   证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

 


参考:
https://blog.csdn.net/Li_Ning_/article/details/52117463
https://blog.csdn.net/sinat_37138973/article/details/72822229