Tallate

该吃吃该喝喝 啥事别往心里搁

kubectl 的安装

Install and Set Up kubectl

kubernetes 含义

Kubernetes Master

Kubernetes Master 主要负责管理集群,它会协调集群内的所有活动,包括:scheduling applications, maintaining applications’ desired state, scaling applications, and rolling out new updates。
Master 实际上是三个进程的集合,它们运行在集群的一个 Master Node 上,这三个进程包括:

  • kube-apiserver:提供REST-API来操作 Kubernetes Objects,包括 pods、services、replicationcontrollers 等,并可用于控制集群状态。
  • kube-controller-manager:通过 apiserver 监听集群状态并做状态转移操作。
  • kube-scheduler:scheduler 可以根据集群的拓扑结构、性能、容量等动态分配资源。

Node

Node 是一台虚拟机或物理电脑,在集群中作为 Worker。Master 管理 Cluster,而 Node 则管理应用。
每个 Node 都有一个 Kubelet,是管理 Node 的媒介,且负责与 Kubernetes Master 进行交互。
每个 Node 还需要有工具来处理容器操作,比如 Docker 或 rkt。
一个生产环境的 cluster 必须由至少 3 个 Node 组成。

kubelet

节点代理,负责和 Kubernetes Master 交互:

  • 向 apiserver 注册节点
  • 其他云计算指定逻辑。

kube-proxy

运行于每个 Node 上的一个网络代理,可以执行简单的 TCP、UDP、SCTP 流转发或提供多后端进程的负载均衡。
相当于在主机和 Cluster 之间创建了一个连接,让我们能直接访问 API。

Pod

一个或多个应用容器集合的抽象,并且包含一些共享资源,包括:

  • 共享存储,如 Volumes;
  • 网络连接,每个 Pod 具有 Cluster 中唯一的一个 IP 地址;
  • 运行每个容器所必须的信息,比如容器镜像的版本、端口等。

Pod 建模了一种“logical host”,可以同时运行多种不同的容器。
当 Pod 所处的 Node 挂了,ReplicaSet会动态地创建新 Pod 来使得 Cluster 回到原来的状态。

Service

Service 定义了 Pod 的逻辑集合及其访问规则,虽然每个 Pod 都有一个唯一的 IP 地址(Cluster 范围内),但是如果没有 Service 的话这些 IP 也是没法暴露到 Cluster 外的,可以通过指定 ServiceSpec 中的type来指定暴露服务的方式:

  • ClusterIP (default) - Exposes the Service on an internal IP in the cluster. This type makes the Service only reachable from within the cluster.
  • NodePort - Exposes the Service on the same port of each selected Node in the cluster using NAT. Makes a Service accessible from outside the cluster using <NodeIP>:<NodePort>. Superset of ClusterIP.
  • LoadBalancer - Creates an external load balancer in the current cloud (if supported) and assigns a fixed, external IP to the Service. Superset of NodePort.
  • ExternalName - Exposes the Service using an arbitrary name (specified by externalName in the spec) by returning a CNAME record with the name. No proxy is used. This type requires v1.7 or higher of kube-dns.

Volumes

Namespaces

Deployments

负责创建和更新应用实例,创建 Deployment 后,Master 会持续监听并在各 Node 中调度应用实例,一旦有实例挂掉或被删掉,Deployment controller 就会用 Cluster 中另一 Node 上的实例取代之。

  • searched for a suitable node where an instance of the application could be run (we have only 1 available node)
  • scheduled the application to run on that Node
  • configured the cluster to reschedule the instance on a new Node when needed

DaemonSet

StatefulSets

ReplicaSet

Jobs

kubernetes 使用

可以采用kubectl命令来和 Cluster 交互,kubectl最常用的操作只有如下 4 种:

  • kubectl get - list resources
  • kubectl describe - show detailed information about a resource
  • kubectl logs - print the logs from a container in a pod
  • kubectl exec - execute a command on a container in a pod

创建Cluster

例子中使用minikube创建 Cluster,然后使用kubectl来和创建的 Cluster 交互:

1
2
3
4
5
6
7
8
9
minikube version
# 启动一个Cluster
minikube start
# 查看客户端(kubectl的版本)和服务端(Kubernetes的版本)的版本
kubectl version
# Cluster的详细信息
kubectl cluster-info
# 获取Cluster内的Node列表
kubectl get nodes
  1. 创建部署单元(Deployment)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 创建Deploymnet
    kubectl create deployment kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1
    # 查看Deployment
    kubectl get deployments

    # 查看Pod
    kubectl get pods
    # 查看Pod内Container的属性,比如使用的是什么镜像
    kubectl describe pods

访问

  1. 使用 proxy 访问 Service 和 Pod
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 创建proxy实例
    kubectl proxy
    # 创建proxy后可以在另一个终端调用REST API来查看Cluster信息
    curl http://localhost:8001/version
    # 查看Pod的名字
    export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}')
    echo Name of the Pod: $POD_NAME
    # 可以通过proxy直接访问Pod
    curl http://localhost:8001/api/v1/namespaces/default/pods/$POD_NAME/proxy/
  2. 查看日志
    1
    2
    # 所有应用发送给STDOUT的信息都会成为Pod容器的日志
    kubectl logs $POD_NAME
  3. 使用 exec 命令访问 Pod
    1
    2
    3
    4
    # 查看Pod内的环境变量
    kubectl exec $POD_NAME env
    # 在Pod中一个容器内启动一个bash会话,相当于登入了该容器
    kubectl exec -ti $POD_NAME bash

Service

  1. 创建 Service
    1
    2
    3
    4
    5
    6
    # 查看Service
    kubectl get services
    # 创建一个新的Service并暴露端口(type=NodePort可以用于占用一个Node上的端口来暴露服务)
    kubectl expose deployment/kubernetes-bootcamp --type="NodePort" --port 8080
    # 查看Service信息
    kubectl describe services/kubernetes-bootcamp
  2. Deployment 与 Service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 8080其实是集群内的一个逻辑端口,可以通过以下命令获取Node上暴露的端口
    export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}')
    # 然后我们可以通过curl访问Node上的端口来确认Service已经被暴露到了Cluster外
    echo NODE_PORT=$NODE_PORT

    # Deployment会自动为Pod创建一个label,可以通过如下命令来查看
    kubectl describe deployment
    # 通过label查看Pod列表和Service列表
    kubectl get pods -l run=kubernetes-bootcamp
    kubectl get services -l run=kubernetes-bootcamp
  3. Service 中的 Pod
    1
    2
    3
    4
    5
    6
    7
    8
    # 先获取Pod的名字
    export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}')
    echo Name of the Pod: $POD_NAME
    # 通过label命令可以设置一个新的label
    kubectl label pod $POD_NAME app=v1
    # 之后可以使用describe 命令来查看这些label,也可以使用label来查询Pod
    kubectl describe pods $POD_NAME
    kubectl get pods -l app=v1
  4. 删除 Service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 删除一个Service
    kubectl delete service -l run=kubernetes-bootcamp
    # 查看服务列表,该服务已经被删除
    kubectl get services
    # curl端口确认不再暴露服务
    curl $(minikube ip):$NODE_PORT
    # 我们可以在Pod中运行curl来确定应用仍运行于Pod中
    kubectl exec -ti $POD_NAME curl localhost:8080
    # 如果需要终止应用,则需要同时删除Deployment

app 扩展

scaling
上图来自官网教程,Scaling 即修改 Deployment 中的 Pod 数。

  1. 扩展 ReplicaSet(Scale Up)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 先看下部署了几个Pod
    kubectl get deployments
    # 查看Deployment创建的ReplicaSet,ReplicaSet名的格式为[DEPLOYMENT-NAME]-[RANDOM-STRING]
    kubectl get rs
    # 将Deployment扩展为4个复制
    kubectl scale deployments/kubernetes-bootcamp --replicas=4
    # 查看Deployment及其中的Pod
    kubectl get deployments
    kubectl get pods -o wide
    # 查看Deployment的时间日志
    kubectl describe deployments/kubernetes-bootcamp
  2. Load Balancing
    1
    2
    3
    4
    5
    # 使用一个NODE_PORT环境变量来保存Node暴露的端口
    export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}')
    echo NODE_PORT=$NODE_PORT
    # curl访问多次,会发现请求被负载均衡到了不同的Pod上
    curl $(minikube ip):$NODE_PORT
  3. 缩小(Scale Down)
    1
    2
    3
    4
    5
    # 再运行一次scale命令来缩小ReplicaSet
    kubectl scale deployments/kubernetes-bootcamp --replicas=2
    # 查看伸缩结果
    kubectl get deployments
    kubectl get pods -o wide

滚动更新(Rolling Update)

  1. 更新应用版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 先看看Cluster里有啥
    kubectl get deployments
    kubectl get pods
    # 查看应用当前使用的镜像版本
    kubectl describe pods
    # 使用set image命令设置应用的镜像版本
    kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=jocatalin/kubernetes-bootcamp:v2
    # 可以通过Pod查看应用的更新过程
    kubectl get pods
  2. 验证更新
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 确认应用正在运行中
    kubectl describe services/kubernetes-bootcamp
    # 用一个NODE_PORT环境变量保存Node暴露的端口,然后用curl确认暴露的服务可以使用
    export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}')
    echo NODE_PORT=$NODE_PORT
    curl $(minikube ip):$NODE_PORT
    # 更新同样可以使用rollout status命令来确认
    kubectl rollout status deployments/kubernetes-bootcamp
    # 查看应用当前使用的镜像的版本
    kubectl describe pods
  3. 回滚(Rollback)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 设置镜像进行更新,但是这个镜像的v10版本是不存在的,因此会引起更新失败
    kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=gcr.io/google-samples/kubernetes-bootcamp:v10
    # 查看Deployment的当前状态,会发现某些Pod运行不正常
    kubectl get deployments
    # 查看Pod状态,describe能提供更多信息
    kubectl get pods
    kubectl describe pods
    # 发现问题,可以使用rollout undo命令来回滚,rollout命令会复原deployment到上一已知状态。实际上所有更新操作都是版本化的,并且每次更新都可以回滚到之前的任意版本
    kubectl rollout undo deployments/kubernetes-bootcamp
    # 接下来再查看Pod的状态可以发现所有的Pod都已经恢复运行了
    kubectl get pods
    kubectl describe pods

k8s Helm

Helm是k8s的包管理工具

  • Chart:每个包称为一个 Chart,一个 Chart 是一个目录(一般情况下会将目录进行打包压缩,形成 name-version.tgz 格式的单一文件,方便传输和存储)。
  • Tiller:Tiller 是 Helm 的服务端,部署在 Kubernetes 集群中。Tiller 用于接收 Helm 的请求,并根据 Chart 生成 Kubernetes 的部署文件( Helm 称为 Release ),然后提交给 Kubernetes 创建应用。Tiller 还提供了 Release 的升级、删除、回滚等一系列功能。
  • Repository:Helm 的软件仓库,Repository 本质上是一个 Web 服务器,该服务器保存了一系列的 Chart 软件包以供用户下载,并且提供了一个该 Repository 的 Chart 包的清单文件以供查询。Helm 可以同时管理多个不同的 Repository。首次安装 Helm 时,它已预配置为使用官方 Kubernetes chart 存储库 repo。该 repo 包含许多精心设计和维护的 charts。此 charts repo 默认以 stable 命名。
  • Release:使用 helm install 命令在 Kubernetes 集群中部署的 Chart 的一个实例称为 Release。在同一个集群上,一个 Chart 可以安装很多次。每次安装都会创建一个新的 release。例如一个 MySQL Chart,如果想在服务器上运行两个数据库,就可以把这个 Chart 安装两次。每次安装都会生成自己的 Release,会有自己的 Release 名称。

原理

Chart Install 过程:

  1. Helm 从指定的目录或者 tgz 文件中解析出 Chart 结构信息
  2. Helm 将指定的 Chart 结构和 Values 信息通过 gRPC 传递给 Tiller
  3. Tiller 根据 Chart 和 Values 生成一个 Release
  4. Tiller 将 Release 发送给 Kubernetes 用于生成 Release

Chart Update 过程:

  1. Helm 从指定的目录或者 tgz 文件中解析出 Chart 结构信息
  2. Helm 将要更新的 Release 的名称和 Chart 结构,Values 信息传递给 Tiller
  3. Tiller 生成 Release 并更新指定名称的 Release 的 History
  4. Tiller 将 Release 发送给 Kubernetes 用于更新 Release

Chart Rollback 过程:

  1. Helm 将要回滚的 Release 的名称传递给 Tiller
  2. Tiller 根据 Release 的名称查找 History
  3. Tiller 从 History 中获取上一个 Release
  4. Tiller 将上一个 Release 发送给 Kubernetes 用于替换当前 Release

语法

表达式
模版表达式:
模版表达式:, 表示去掉表达式输出结果前面和后面的空格。去掉前面空格可以这么写,去掉后面空格

待补充

实战

实战中可以将配置单独存放一个代码库,主要包含:

  • 镜像版本配置
  • 中间件配置
  • 外部依赖
  • 业务配置

参考

  1. Tutorials
    Learn Kubernetes Basics
    Configuring Redis using a ConfigMap
    使用 k8s 搭建 Redis 集群
    Exposing an External IP Address to Access an Application in a Cluster
    如何使用 k8s 搭建一个无状态服务
    StatefulSet Basics
    如何使用 k8s 搭建一个有状态服务
    AppArmor
    Using Source IP
    CICD
    https://www.linux.com/tutorials/set-cicd-pipeline-jenkins-pod-kubernetes-part-2/
    https://www.linux.com/tutorials/run-and-scale-distributed-crossword-puzzle-app-cicd-kubernetes-part-3/
    https://www.linux.com/tutorials/set-cicd-distributed-crossword-puzzle-app-kubernetes-part-4/
    Helm

在 awk、sed、cut 三个命令中,awk 是功能最强大的,基本能实现所有字符串操作,平时常用于较复杂的日志分析,不过比起别的命令来也会相对复杂一点。

阅读全文 »

  • TCP是面向连接
    所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性
  • TCP 提供可靠交付
    通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。
  • TCP 是面向字节流
    发送的时候发的是一个流,没头没尾。
  • 拥塞控制
    TCP意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。

TCP 优点

  • 可靠,稳定
    TCP 的可靠体现在 TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。

TCP 缺点

慢,效率低,占用系统资源高,易被攻击
TCP 在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的 CPU、内存等硬件资源。 而且,因为 TCP 有确认机制、三次握手机制,这些也导致 TCP 容易被人利用,实现 DOS、DDOS、CC 等攻击。

TCP的数据包格式

TCP数据包格式

  1. 源端口号和目标端口号,用于确定数据应该发给哪个应用;
  2. 包序号
    用于解决乱序问题;
  3. 确认序号
    解决丢包问题,如果没有收到就重新发送,直到送达。
  4. 状态位
    例如SYN是发起一个连接;
    ACK是回复;
    RST是重新连接;
    FIN是结束连接。
  5. 窗口大小
    TCP要做到流量控制,通信双方各声明一个窗口,标识自己当前的处理能力。

连接的建立 - 三次握手

TCP三次握手
一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于 ESTABLISHED 状态,因为它一发一收成功了。服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收了。
从连接建立的流程中可见,三次握手除了双方建立连接外,还能确定TCP包的序号问题。

总而言之:

  1. 首先 Client 端发送连接请求报文,Server 段接受连接后回复 ACK 报文,并为这次连接分配资源。
  2. Client 端接收到 ACK 报文后也向 Server 段发生 ACK 报文,并分配资源,这样 TCP 连接就建立了。
    TCP三次握手
    最初两端的 TCP 进程都处于 CLOSED 关闭状态,A(Client)主动打开连接,而 B(Server)被动打开连接。(A、B 关闭状态 CLOSED——B 创建 TCB,进入 LISTEN 状态,等待 A 请求——A 同步已发送状态 SYN-SENT——B 同步收到状态 SYN-RCVD——A、B 连接已建立状态 ESTABLISHED)
  3. 第一次握手:起初两端都处于 CLOSED 关闭状态,A(Client)将标志位 SYN 置为 1,随机产生一个值 seq=x,并将该数据包发送给 B(Server),A(Client)进入 SYN-SENT 状态,等待 B(Server)确认;
  4. 第二次握手:B(Server)收到连接请求报文段后,如同意建立连接,则向 A(Client)发送确认,在确认报文段中(SYN=1,ACK=1,确认号 ack=x+1,初始序号 seq=y),B(Server)TCP 服务器进程进入 SYN-RCVD(同步收到)状态;
  5. 第三次握手:TCP 客户进程收到 B(Server)的确认后,要向 B(Server)给出确认报文段(ACK=1,确认号 ack=y+1,序号 seq=x+1)(初始为 seq=x,第二个报文段所以要+1),ACK 报文段可以携带数据,不携带数据则不消耗序号。TCP 连接已经建立,A 进入 ESTABLISHED(已建立连接)。
    当 B 收到 A 的确认后,也进入 ESTABLISHED 状态。

    TCB 传输控制块 Transmission Control Block,存储每一个连接中的重要信息,如 TCP 连接表,到发送和接收缓存的指针,到重传队列的指针,当前的发送和接收序号。

一些问题:

  1. 为什么 A 还要发送一次确认呢?可以二次握手吗?
    主要为了防止已失效的连接请求报文段突然又传送到了 B,因而产生错误。如 A 发出连接请求,但因连接请求报文丢失而未收到确认,于是 A 再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,A 发出了两个连接请求报文段,其中第一个丢失,第二个到达了 B,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达 B,此时 B 误认为 A 又发出一次新的连接请求,于是就向 A 发出确认报文段,同意建立连接,不采用三次握手,只要 B 发出确认,就建立新的连接了,此时 A 不理睬 B 的确认且不发送数据,则 B 一致等待 A 发送数据,浪费资源。
  2. Server 端易受到 SYN 攻击?
    服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到 SYN 洪泛攻击,SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向 Server 不断地发送 SYN 包,Server 则回复确认包,并等待 Client 确认,由于源地址不存在,因此 Server 需要不断重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
    防范 SYN 攻击措施:降低主机的等待时间使主机尽快的释放半连接的占用,短时间受到某 IP 的重复 SYN 则丢弃后续请求。

连接的断开 - 四次挥手

TCP四次挥手
假设 Client 端发起中断连接请求,也就是发送 FIN 报文。Server 端接到 FIN 报文后,意思是说”我 Client 端没有数据要发给你了”,但是如果你还有数据没有发送完成,则不必急着关闭 Socket,可以继续发送数据。所以你先发送 ACK,”告诉 Client 端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候 Client 端就进入 FIN_WAIT 状态,继续等待 Server 端的 FIN 报文。当 Server 端确定数据已发送完成,则向 Client 端发送 FIN 报文,”告诉 Client 端,好了,我这边数据发完了,准备好关闭连接了”。Client 端收到 FIN 报文后,”就知道可以关闭连接了,但是他还是不相信网络,怕 Server 端不知道要关闭,所以发送 ACK 后进入 TIME_WAIT 状态,如果 Server 端没有收到 ACK 则可以重传。“,Server 端收到 ACK 后,”就知道可以断开连接了”。Client 端等待了 2MSL 后依然没有收到回复,则证明 Server 端已正常关闭,那好,我 Client 端也可以关闭连接了。Ok,TCP 连接就这样关闭了!

数据传输结束后,通信的双方都可释放连接,A 和 B 都处于 ESTABLISHED 状态。(A、B 连接建立状态 ESTABLISHED——A 进入等待 1 状态 FIN-WAIT-1——B 关闭等待状态 CLOSE-WAIT——A 进入等待 2 状态 FIN-WAIT-2——B 最后确认状态 LAST-ACK——A 时间等待状态 TIME-WAIT——B、A 关闭状态 CLOSED)

  1. A 的应用进程先向其 TCP 发出连接释放报文段(FIN=1,序号 seq=u),并停止再发送数据,主动关闭 TCP 连接,进入 FIN-WAIT-1(终止等待 1)状态,等待 B 的确认。
  2. B 收到连接释放报文段后即发出确认报文段,(ACK=1,确认号 ack=u+1,序号 seq=v),B 进入 CLOSE-WAIT(关闭等待)状态,此时的 TCP 处于半关闭状态,A 到 B 的连接释放。
  3. A 收到 B 的确认后,进入 FIN-WAIT-2(终止等待 2)状态,等待 B 发出的连接释放报文段。
  4. B 没有要向 A 发出的数据,B 发出连接释放报文段(FIN=1,ACK=1,序号 seq=w,确认号 ack=u+1),B 进入 LAST-ACK(最后确认)状态,等待 A 的确认。
  5. A 收到 B 的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A 进入 TIME-WAIT(时间等待)状态。此时 TCP 未释放掉,需要经过时间等待计时器设置的时间 2MSL 后,A 才进入 CLOSED 状态。

总而言之:

  1. 刚开始A发送断开连接请求,进入FIN_WAIT_1状态;
  2. B接收请求进入CLOSED_WAIT状态,并发回响应;
  3. A接收B响应,进入FIN_WAIT_2状态;
  4. B发送断开连接请求,并进入LAST_ACK状态;
  5. A接收请求,进入TIME_WAIT状态,并发回响应;
    A会在发出响应2MSL后自动进入CLOSED状态。
    MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。
  6. B接收响应后进入CLOSED状态。

该流程中让人比较困惑的问题是:

  1. 为什么A断开连接后,第4步还要B在反向发起一次断开连接?
    不能直接断开,因为A单方面断开连接时,A不知道B是不是还有事情要处理。
  2. 如果第4步B没有重新发起断开连接(只有2次挥手),连接怎么断开?
    如果这期间B宕机了没有重新发起断开连接,A将永远停留在FIN_WAIT_2的状态,TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。
  3. 第5步如果A响应没有返回给B,B怎么断开?
    此时B会重新发起一次断开请求,因而TCP要求A最后等待一段时间TIME_WAIT,这个时间要足够长,长到如果B没有收到ACK,B可以重试且时间足够到达。
    不过,还有一个异常情况就是,B 超过了 2MSL 的时间,依然没有收到它发的 FIN 的 ACK,怎么办呢?按照 TCP 的原理,B 当然还会重发 FIN,这时A再收到这个包后,会直接返回RST,B就知道早已断开连接了。
  4. TIME_WAIT
    TIME_WAIT 状态容易被人误解,比如当使用三方系统的服务时,看到系统 TIME_WAIT 数量特别高,于是赖对方没有及时把连接释放掉,实际上TIME_WAIT 产生在主动断开连接的一方
  5. 为什么 A 在 TIME-WAIT 状态必须等待 2MSL 的时间?(MSL 最长报文段寿命 Maximum Segment Lifetime,MSL=2)
    原因有 2:
    保证 A 发送的最后一个 ACK 报文段能够到达 B。这个 ACK 报文段有可能丢失,使得处于 LAST-ACK 状态的 B 收不到对已发送的 FIN+ACK 报文段的确认,B 超时重传 FIN+ACK 报文段,而 A 能在 2MSL 时间内收到这个重传的 FIN+ACK 报文段,接着 A 重传一次确认,重新启动 2MSL 计时器,最后 A 和 B 都进入到 CLOSED 状态,若 A 在 TIME-WAIT 状态不等待一段时间,而是发送完 ACK 报文段后立即释放连接,则无法收到 B 重传的 FIN+ACK 报文段,所以不会再发送一次确认报文段,则 B 无法正常进入到 CLOSED 状态。
    防止“已失效的连接请求报文段”出现在本连接中。A 在发送完最后一个 ACK 报文段后,再经过 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
  6. 为什么连接的时候是三次握手,关闭的时候却是四次握手?
    因为当 Server 端收到 Client 端的 SYN 连接请求报文后,可以直接发送 SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当 Server 端收到 FIN 报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉 Client 端,”你发的 FIN 报文我收到了”。只有等到我 Server 端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。故需要四步握手。
  7. 为什么 TIME_WAIT 状态需要经过 2MSL(最大报文段生存时间)才能返回到 CLOSE 状态?
    虽然按道理,四个报文都发送完毕,我们可以直接进入 CLOSE 状态了,但是我们必须假象网络是不可靠的,有可能最后一个 ACK 丢失。所以 TIME_WAIT 状态就是用来重发可能丢失的 ACK 报文。
  8. 如何优化
    我们可以通过修改系统参数来优化服务器
    tcp_tw_reuse: 是否重用处于 TIME_WAIT 状态的 TCP 链接 (设为 true)
    tcp_max_tw_buckets: 处于 TIME_WAIT 状态的 SOCKET 最大数目 (调大,这个参数千万不要调小了)
    tcp_fin_timeout: 处于 FIN_WAIT_2 的时间 (调小)

TCP状态机

上面的握手和挥手流程汇总为状态机如下图所示:
TCP状态机

累计确认(cumulative acknowledgment,或称为累计应答)

TCP中并不是发1收1,而是使用一个缓冲区保存数据包,这个缓冲区称为窗口,发送端的发送窗口如下图所示:
TCP发送窗口

  1. 发送了并且已经确认的。
  2. 发送了并且尚未确认的。
  3. 没有发送,但是已经等待发送的。
  4. 没有发送,并且暂时还不会发送的。
  • LastByteAcked:第一部分和第二部分的分界线
  • LastByteSent:第二部分和第三部分的分界线
  • LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线

为什么会有第3和第4部分?难道这些不都是等待发送的吗?其实区分第3和第4部分的主要目的是流量控制,在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window。这个窗口的大小应该等于上面的第2部分加上第3部分,超过了这个窗口的接收端处理不过来,就不发送了,作为第4部分。

接收端同样也有一个接收窗口:
TCP接收窗口

  1. 接受并且确认过的。
  2. 还没接收,但是马上就能接收的。
  3. 还没接收,也没法接收的。
  • MaxRcvBuffer:最大缓存的量;
  • LastByteRead 之后是已经接收了,但是还没被应用层读取的;
  • NextByteExpected 是第一部分和第二部分的分界线。

顺序问题与丢包问题

TCP中的重发有两种:

  1. 超时重试
    对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。
    这个时间不宜过短,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)
    超时间隔加倍:每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
  2. 快速重传
    超时重传存在的问题是超时周期可能会比较长,为了加快重传,TCP采用一种快速重传机制:
    当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。
    例如,接收方发现 6 收到了,8 也收到了,但是 7 还没来,那肯定是丢了,于是发送 6 的 ACK,要求下一个是 7。接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。
  3. Selective Acknowledgment (SACK)
    接收方可以将缓存的地图发送给发送方,例如发送ACK6、SACK8,SACK8表示已接收但未处理的,有了地图,发送方一下子就能看出来是 7 丢了。

流量控制

当发送未确认窗口中最早的一个包接收到了确认,则窗口将前移一格:
TCP接收窗口
上图中,5接收到了确认,此时窗口前移一格,第14个包可以发送了。
TCP接收窗口移动
如果接收方处理太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。
假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包 6 确认后,窗口大小就不能再是 9 了,就要缩小一个变为 8。
那么缩小后什么时候恢复呢?发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。

拥塞控制

拥塞控制也是通过控制窗口大小来实现的,前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满。

TCP的拥塞控制主要是为了避免丢包和超时重传问题:

  • 如果窗口不经控制——像UDP那样——很有可能发送的数据量超过中间设备的承载能力,多出来的包就会被丢弃,即发生了丢包,这是我们不希望看到的。

  • 如果在这些设备上加缓存,处理不过来的先保存在缓存队列里,这样虽然不会丢失,但是会增加时延,如果时延达到一定程度,就会导致超时重传

  • 慢启动(指数增长)
    刚开始不清楚网络情况,因此发送数据包时一次只能发1个,后来按2、4、8的指数性增长速度来增长;

  • 线性增长
    当超过一个阈值ssthresh=65535时,可能速度达到了网络性能,这时会慢下来,变成线程增长,每收到一个确认后,cwnd才会增加1/cwnd

  • 指数递减
    当发生了丢包,需要超时重传时,会设置ssthresh=cwnd/2,并将cwnd设置为1,重新开始慢启动,这种减速方式的问题是太过激进,从原来的高速马上减到1,会造成明显的网络卡顿,因为这个问题,一般会采用快速重传算法

  • 快速重传算法
    当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。
    TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是说没有立刻减到1。

为什么要使用 Nginx

Nginx 优点

  1. 轻量级,采用 C 进行编写,同样的 web 服务,会占用更少的内存及资源
  2. 抗并发,nginx 以 epoll 和 kqueue 作为开发模型,处理请求是异步非阻塞的,负载能力比 apache 高很多,而 apache 则是阻塞型的。在高并发下 nginx 能保持低资源低消耗高性能 ,而 apache 在 PHP 处理慢或者前端压力很大的情况下,很容易出现进程数飙升,从而拒绝服务的现象。
  3. nginx 处理静态文件好,静态处理性能比 apache 高三倍以上
  4. nginx 的设计高度模块化,编写模块相对简单
  5. nginx 配置简洁,正则配置让很多事情变得简单,而且改完配置能使用 -t 测试配置有没有问题,apache 配置复杂 ,重启的时候发现配置出错了,会很崩溃
  6. nginx 作为负载均衡服务器,支持 7 层负载均衡
  7. nginx 本身就是一个反向代理服务器,而且可以作为非常优秀的邮件代理服务器
  8. 启动特别容易, 并且几乎可以做到 7*24 不间断运行,即使运行数个月也不需要重新启动,还能够不间断服务的情况下进行软件版本的升级
  9. 社区活跃,各种高性能模块出品迅速

Nginx 优点(说出原因)

  1. Nginx 在核心代码都使用了与操作系统无关的代码实现,在与操作系统相关的系统调用上则分别针对各个操作系统都有独立实现,这最终造就了 Nginx 的可移植性。
  2. 非阻塞、高并发连接:处理 2-3 万并发连接数,官方监测能支持 5 万并发
  3. 内存消耗小:开启 10 个 nginx 才占 150M 内存,Nginx 采取了分阶段资源分配技术
    nginx 处理静态文件好,耗费内存少
  4. 内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上。
    节省宽带:支持 GZIP 压缩,可以添加浏览器本地缓存
    稳定性高:宕机的概率非常小
  5. master/worker 结构:一个 master 进程,生成一个或者多个 worker 进程
    接收用户请求是异步的:浏览器将请求发送到 nginx 服务器,它先将用户请求全部接收下来,再一次性发送给后端 web 服务器,极大减轻了 web 服务器的压力
    一边接收 web 服务器的返回数据,一边发送给浏览器客户端
    网络依赖性比较低,只要 ping 通就可以负载均衡
    可以有多台 nginx 服务器
  6. 事件驱动:通信机制采用 epoll 模型

Apache 优点

  1. apache 的 rewrite 比 nginx 强大,在 rewrite 频繁的情况下,用 apache
  2. apache 发展到现在,模块超多,基本想到的都可以找到
  3. apache 更为成熟,少 bug ,nginx 的 bug 相对较多
  4. apache 超稳定
  5. apache 对 PHP 支持比较简单,nginx 需要配合其他后端用
  6. apache 在处理动态请求有优势,nginx 在这方面是鸡肋,一般动态请求要 apache 去做,nginx 适合静态和反向。
  7. apache 仍然是目前的主流,拥有丰富的特性,成熟的技术和开发社区

Nginx 和 Apache 区别总结

两者最核心的区别在于 apache 是同步多进程模型,一个连接对应一个进程,而 nginx 是异步的,多个连接(万级别)可以对应一个进程
一般来说,需要性能的 web 服务,用 nginx 。如果不需要性能只求稳定,更考虑 apache ,后者的各种功能模块实现得比前者,例如 ssl 的模块就比前者好,可配置项多。epoll(freebsd 上是 kqueue ) 网络 IO 模型是 nginx 处理性能高的根本理由,但并不是所有的情况下都是 epoll 大获全胜的,如果本身提供静态服务的就只有寥寥几个文件,apache 的 select 模型或许比 epoll 更高性能。当然,这只是根据网络 IO 模型的原理作的一个假设,真正的应用还是需要实测了再说的。
更为通用的方案是,前端 nginx 抗并发,后端 apache 集群,配合起来会更好。

接入层演进

通过研究接入层的发展历程,我们可以一窥 Nginx 在互联网架构中的地位。

接入层结构

接入层结构
可以看到,每一个下游都有多个上游调用,只需要做到,每一个上游都均匀访问每一个下游,就能实现整体的均匀分摊。

  1. 客户端层->反向代理层
    DNS 轮询
    DNS-server 对于一个域名配置了多个解析 ip,每次 DNS 解析请求来访问 DNS-server,会轮询返回这些 ip,保证每个 ip 的解析概率是相同的。这些 ip 就是 nginx 的外网 ip,以做到每台 nginx 的请求分配也是均衡的。
  2. 反向代理层->站点层
    反向代理层到站点层的负载均衡,是通过“nginx”实现的。
    修改 nginx.conf,可以实现多种均衡策略:
    2.1 请求轮询:和 DNS 轮询类似,请求依次路由到各个 web-server;
    2.2 最少连接路由:哪个 web-server 的连接少,路由到哪个 web-server;
    2.3 ip 哈希:按照访问用户的 ip 哈希值来路由 web-server,只要用户的 ip 分布是均匀的,请求理论上也是均匀的,ip 哈希均衡方法可以做到,同一个用户的请求固定落到同一台 web-server 上,此策略适合有状态服务,例如 session;

    session 不推荐放到站点层,后期扩展会有问题,更好的方案是放到数据层。

  3. 站点层->服务层
    站点层到服务层的负载均衡,是通过“服务连接池”实现的。
    上游连接池会建立与下游服务多个连接,每次请求会“随机”选取连接来访问下游服务。除了负载均衡,服务连接池还能够实现故障转移、超时处理、限流限速、ID 串行化等诸多功能。
  4. 服务层->数据层
    在数据量很大的情况下,由于数据层(db/cache)涉及数据的水平切分,所以数据层的负载均衡更为复杂一些,它分为“数据的均衡”,与“请求的均衡”。
    数据的均衡是指:水平切分后的每个服务(db/cache),数据量是均匀的。
    请求的均衡是指:水平切分后的每个服务(db/cache),请求量是均匀的。

接入层演进

  1. 单机架构
    客户端用 DNS 解析出来的 IP 就是 web 服务器的地址。
    缺点:
    • 单点;
    • 扩展性差。
  2. DNS 轮询
    在 DNS 服务器上多配几个 IP,由域名服务器的解析策略实现负载均衡。
    缺点:
    • 非高可用,因为一个 web 服务器挂掉后 DNS 服务器仍然会将请求解析到该服务器对应的 IP 上;
    • 扩容非实时,DNS 服务器有一个配置生效的延时时间;
    • 暴露太多外网 IP。
  3. 反向代理 Nginx
    DNS 解析到 Nginx 的 IP,然后由 Nginx 将请求负载均衡到 web 服务器。
    缺点:基本解决了上一个架构存在的问题,且可以利用 Nginx 的探活机制实现 web 服务器的高可用,但是此时 Nginx 也会成为一个单点。
  4. keepalived
    两台 Nginx 组成集群,分别部署上 keepalived,设置成相同的虚 IP,保证 Nginx 的高可用。当一台 Nginx 挂了,keepalived 能够探测到并将流量迁移到另一台 Nginx 上,整个过程对调用方透明。
    缺点:
    • 资源利用率低;
    • 扩容不方便,如果吞吐量超过 Nginx 性能上线,要加机器配置起来比较麻烦。
  5. lvs/f5
    DNS 解析出来的 IP 是 lvs 的地址。由 lvs 反向代理 Nginx 服务器,lvs 的机器上部署 keepalived+VIP 实现高可用;
    f5 的性能比 lvs 更高,但是成本也会更高。
  6. DNS 轮询
    水平扩展才是解决性能问题的根本方案,能够通过加机器扩充性能的方案才具备最好的扩展性。
    可扩展高可用接入层架构
    • 通过 DNS 轮询来线性扩展入口 lvs 层的性能;
    • 通过 keepalived 保证高可用;
    • 通过 lvs 来扩展多个 Nginx;
    • 通过 Nginx 实现对业务服务器的七层负载均衡。

使用 Nginx

手动安装 Nginx

  1. 配置
    创建目录/var/temp/nginx
    这个目录保存临时文件,在安装配置中指定:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ./configure \
    --prefix=/usr/local/nginx \
    --pid-path=/var/run/nginx/nginx.pid \
    --lock-path=/var/lock/nginx.lock \
    --error-log-path=/var/log/nginx/error.log \
    --http-log-path=/var/log/nginx/access.log \
    --with-http_gzip_static_module \
    --http-client-body-temp-path=/var/temp/nginx/client \
    --http-proxy-temp-path=/var/temp/nginx/proxy \
    --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
    --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
    --http-scgi-temp-path=/var/temp/nginx/scgi
  2. 安装
    1
    2
    make
    make install
  3. 配置环境变量
    /etc/profile中编辑,这样就可以直接使用 nginx 命令启动了
    1
    export PATH=$PATH:/usr/local/nginx/sbin
  4. 启动
    1
    2
    3
    nginx
    # 指定配置文件
    nginx -c /usr/local/nginx/conf/nginx.conf
    如果不指定-c,nginx 在启动时默认加载 conf/nginx.conf 文件,此文件的地址也可以在编译安装 nginx 时指定./configure 的参数(–conf-path= 指向配置文件(nginx.conf))
  5. 停止
    1
    2
    nginx -s stop # 相当于先查出nginx进程id再kill
    nginx -s quit # 建议使用,这种方法是等nginx进程的任务处理完毕后再停止
  6. 重启
    1
    2
    nginx -s quit
    nginx
    要想在修改配置文件 nginx.conf 后生效:
    1
    nginx -s reload

开机自启

  1. 创建/etc/init.d/nginx
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    #!/bin/bash
    # nginx Startup script for the Nginx HTTP Server
    # it is v.0.0.2 version.
    # chkconfig: - 85 15
    # description: Nginx is a high-performance web and proxy server.
    # It has a lot of features, but it's not for everyone.
    # processname: nginx
    # pidfile: /var/run/nginx.pid
    # config: /usr/local/nginx/conf/nginx.conf
    nginxd=/usr/local/nginx/sbin/nginx
    nginx_config=/usr/local/nginx/conf/nginx.conf
    nginx_pid=/var/run/nginx.pid
    RETVAL=0
    prog="nginx"
    # Source function library.
    . /etc/rc.d/init.d/functions
    # Source networking configuration.
    . /etc/sysconfig/network
    # Check that networking is up.
    [ ${NETWORKING} = "no" ] && exit 0
    [ -x $nginxd ] || exit 0
    # Start nginx daemons functions.
    start() {
    if [ -e $nginx_pid ];then
    echo "nginx already running...."
    exit 1
    fi
    echo -n $"Starting $prog: "
    daemon $nginxd -c ${nginx_config}
    RETVAL=$?
    echo
    [ $RETVAL = 0 ] && touch /var/lock/subsys/nginx
    return $RETVAL
    }
    # Stop nginx daemons functions.
    stop() {
    echo -n $"Stopping $prog: "
    killproc $nginxd
    RETVAL=$?
    echo
    [ $RETVAL = 0 ] && rm -f /var/lock/subsys/nginx /var/run/nginx.pid
    }
    # reload nginx service functions.
    reload() {
    echo -n $"Reloading $prog: "
    #kill -HUP `cat ${nginx_pid}`
    killproc $nginxd -HUP
    RETVAL=$?
    echo
    }
    # See how we were called.
    case "$1" in
    start)
    start
    ;;
    stop)
    stop
    ;;
    reload)
    reload
    ;;
    restart)
    stop
    start
    ;;
    status)
    status $prog
    RETVAL=$?
    ;;
    *)
    echo $"Usage: $prog {start|stop|restart|reload|status|help}"
    exit 1
    esac
    exit $RETVAL
  2. 设置文件访问权限
    1
    chmod a+x /etc/init.d/nginx
  3. 设置开机加载
    /etc/rc.local中加入一行
    1
    /etc/init.d/nginx start

通过 Docker 运行 Nginx

手动进行 Nginx 配置十分繁琐,可以使用 Docker 来简化部署流程:

1
docker run -d -p 80:80 nginx

Nginx 原理 - 进程

Nginx 代码的模块化结构

Nginx 的代码是由一个核心和一系列的模块组成的。
核心主要用于提供 WebServer 的基本功能,以及 Web 和 Mail 反向代理的功能;还用于启用网络协议,创建必要的运行时环境以及确保不同的模块之间平滑地进行交互。
不过,大多跟协议相关的功能和应用特有的功能都是由 nginx 的模块实现的。
换句话说, 每一个功能或操作都由一个模块来实现
这些功能模块大致可以分为事件模块、阶段性处理器、输出过滤器、变量处理器、协议、upstream 和负载均衡几个类别,这些共同组成了 nginx 的 http 功能。
事件模块主要用于提供 OS 独立的(不同操作系统的事件机制有所不同)事件通知机制如 kqueue 或 epoll 等。
协议模块则负责实现 nginx 通过 http、tls/ssl、smtp、pop3 以及 imap 与对应的客户端建立会话。
在 Nginx 内部,进程间的通信是通过模块的 pipelinechain 实现的。
换句话说,每一个功能或操作都由一个模块来实现。例如:压缩、通过 FastCGI 或 uwsgi 协议与 upstream 服务器通信、以及与 memcached 建立会话等。

进程结构

一个 Nginx 服务器实例由一个 master 进程和多个 worker 进程组成。
进程结构
master进程主要用来管理 worker 进程,还有一些对整个服务器的初始化和日志记录等工作。
管理 worker 的过程:接收来自外界的信号,向各 worker 进程发送 信号 ,监控 worker 进程的运行状态,当 worker 进程退出后(异常情况下),会自动重新启动(fork)新的 worker 进程。

master 主要功能

  • 读取并验证配置信息;
  • 创建、绑定及关闭套接字;
  • 启动、终止 worker 进程及维护 worker 进程的个数;
  • 无须中止服务而重新配置工作;
  • 控制非中断式程序升级,启用新的二进制程序并在需要时回滚至老版本;
  • 重新打开日志文件;
  • 编译嵌入式 perl 脚本

对请求的实际处理由 worker 负责,且每个请求只能由一个 worker 负责(一对一)。在启动时,创建一组初始的监听套接字,HTTP 请求和响应之时,worker 连续接收、读取和写入套接字。

worker 主要功能

  • 接收、传入并处理来自客户端的连接;
  • 提供反向代理及过滤功能;
  • nginx 任何能完成的其它任务

Nginx 的启动

nginx 启动后,在 unix 系统中会以 daemon 的方式在后台运行,后台进程包含一个 master 进程和多个 worker 进程(你可以理解为工人和管理员)。

Nginx 处理连接过程

nginx 不会为每个连接派生进程或线程,而是由 worker 进程通过监听共享套接字接受新请求,并且使用高效的 循环 来处理数千个连接。
Nginx 不使用仲裁器或分发器来分发连接,这个工作由操作系统内核机制完成。 监听套接字 在启动时就完成初始化,worker 进程通过这些套接字接受、读取请求和输出响应。

一次请求过程大概执行过程为:

  1. nginx 在启动时,会解析配置文件,得到需要监听的端口与 ip 地址,然后在 nginx 的 master 进程里面先初始化好这个监控的 socket,再进行 listen(listenfd);
  2. 由 master 进程 fork 出多个 worker 进程;
  3. 此时客户端可以向 nginx 发起连接了,客户端会与 nginx 进行三次握手(TCP),与 nginx 建立好一个连接;
  4. 所有 worker 进程的 listenfd 会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有 worker 进程会在注册 listenfd 读事件前抢 accept_mutex,抢到互斥锁的那个进程注册 listenfd 读事件,然后在读事件里调用 accept 接受该连接
  5. 当一个 worker 进程在 accept 这个连接之后,然后创建 nginx 对连接的封装,即 ngx_connection_t 结构体,就开始读取请求、解析请求、处理请求( 异步非阻塞 ),主要是根据事件调用相应的事件处理模块,如 http 模块与客户端进行数据的交换
  6. 产生数据后,再返回给客户端,最后才断开连接,或者由客户端主动关闭连接。

进程间通信

在 Nginx 内部,进程间的通信是通过模块的 pipelinechain 实现的,其原理是信号机制,master 对 worker 进程采用信号进行控制。

事件驱动

所谓事件驱动架构,简单来说,就是由一些事件发生源来产生事件,由一个或多个事件收集器(epolld 等)来收集、分发事件,然后许多事件处理器会注册自己感兴趣的事件,同时会“消费”这些事件。nginx 不会使用进程或线程作为事件消费者,只能是某个模块,当前进程调用模块。
传统 web 服务器(如 Apache)的所谓事件局限在 TCP 连接建立、关闭上,其他读写都不再是事件驱动,这时会退化成按序执行每个操作的批处理模式,这样每个请求在连接建立后都将始终占用系统资源,直到连接关闭才会释放资源。大大浪费了内存、cpu 等资源。并且把一个进程或线程作为事件消费者。 传统 Web 服务器每个事件消费者独占一个进程资源,相对来说,Nginx 只是被事件分发者进程短期调用而已。
nginx 采用多 worker 的方式来处理请求,每个 worker 里面只有一个主线程,那能够处理的并发数很有限,多少个 worker 就能处理多少个并发,那么何来的高并发呢?
其实,Nginx 是采用了异步非阻塞的 IO 模型来处理请求的(epoll),异步的概念是和同步相对的,也就是不同事件之间不是同时发生的。非阻塞的概念是和阻塞对应的,阻塞是事件按顺序执行,每一事件都要等待上一事件的完成,而非阻塞是如果事件没有准备好,这个事件可以直接返回,过一段时间再进行处理询问,这期间可以做其他事情。
请求的多阶段异步处理只能基于事件驱动框架实现,就是把一个请求的处理过程按照事件的触发方式分为多个阶段,每个阶段都可以有事件收集、分发器(epoll 等)来触发。比如一个 http 请求可以分为七个阶段。
每种事件都有一个事件队列,按触发的先后顺序处理。

惊群现象

惊群是多个子进程在同一时刻监听同一个端口引起的;
Nginx 解决方法:同一个时刻只能有唯一一个 worker 子进程监听 web 端口,此时新连接事件只能唤醒唯一正在监听端口的 worker 子进程。这可以通过锁或互斥量实现。

为什么不使用多线程

  • Apache: 创建多个进程或线程,而每个进程或线程都会为其分配 cpu 和内存(线程要比进程小的多,所以 worker 支持比 perfork 高的并发),并发过大会榨干服务器资源。
  • Nginx: 采用单线程来异步非阻塞处理请求(管理员可以配置 Nginx 主进程的工作进程的数量)(epoll),不会为每个请求分配 cpu 和内存资源,节省了大量资源,同时也减少了大量的 CPU 的上下文切换。所以才使得 Nginx 支持更高的并发。

模块

模块命名

ngx_http_[module-name]_[main|srv|loc]_conf_t
前缀表示模块名,后面表示模块运行在哪一层

模块化结构

Nginx由内核和一系列模块组成,内核提供web服务的基本功能,如启用网络协议,创建运行环境,接收和分配客户端请求,处理模块之间的交互。Nginx的各种功能和操作都由模块来实现。
Nginx的模块从结构上分为核心模块、基础模块和第三方模块。

  • 核心模块: HTTP模块、EVENT模块和MAIL模块
  • 基础模块: HTTP Access模块、HTTP FastCGI模块、HTTP Proxy模块和HTTP Rewrite模块
  • 第三方模块: HTTP Upstream Request Hash模块、Notice模块和HTTP Access Key模块及用户自己开发的模块

这样的设计使Nginx方便开发和扩展,也正因此才使得Nginx功能如此强大。Nginx的模块默认编译进nginx中,如果需要增加或删除模块,需要重新编译Nginx,这一点不如Apache的动态加载模块方便。如果有需要动态加载模块,可以使用由淘宝网发起的web服务器Tengine,在nginx的基础上增加了很多高级特性,完全兼容Nginx,已被国内很多网站采用。

模块大致结构如下图所示。
Nginx模块结构
Nginx模块,简单地讲,就是:在特定地方调用的函数。

模块执行过程

nginx的配置 指令作用域 分为以下几种:main,server,location
main作用域的范围为整个配置文件,而server是指某个具体的服务器(通过端口号来区分),而location就是指要访问这个server的哪个location。

Nginx 本身做的工作实际很少,当它接到一个 HTTP 请求时,它仅仅是通过查找配置文件将此次请求映射到一个 locationblock,而此 location 中所配置的各个指令则会启动不同的模块去完成工作。
通常一个 location 中的指令会涉及一个 handler 模块和多个 filter 模块(当然,多个 location 可以复用同一个模块)。handler 模块负责处理请求,完成响应内容的生成,而 filter 模块对响应内容进行处理。
模块处理请求的大致过程如下图所示。
Nginx模块执行流程

http index模块(ngx_http_index_module)

定义将要被作为默认页的文件。 文件的名字可以包含变量。 文件以配置中指定的顺序被 nginx 检查。 列表中的最后一个元素可以是一个带有绝对路径的文件。

1
2
3
4
location / {
root /home/ftpuser/www;
index index.html index.$haha.html index.htm;
}

需要注意的是,index 文件会引发内部重定向,请求可能会被其它 location 处理。
比如下面的配置,请求”/“实际上将会在第二个location中作为”/index.html”被处理:

1
2
3
4
5
6
location = / {
index index.html;
}
location / {
...
}

http log模块(ngx_http_log_module)

1
2
3
4
log_format  gzip '$remote_addr-$remote_user[$time_local]'
:'$request$status $bytes_sent'
:'" $ http _ referer" "$http_user_agent" "$gzip_ratio"';
access_log /spool/logs/nginx-access.log gzip buffer=32k;

指令 access_log 指派路径、格式和缓存大小:

1
2
3
4
# 格式
access_log path [format [buffer=size | off ]
# 默认
access_log log/access.log combined

其中参数 “off” 将清除当前级别的所有 access_log 指令。如果未指定格式,则使用预置的 “combined” 格式。缓存不能大于能写入磁盘的文件的最大值(在 FreeBSD 3.0-6.0 ,缓存大小无此限制)。
指令 log_format 指定日志格式:

1
2
3
4
# 格式
log_format name format [format ...]
# 默认
log_format combined "..."

Access模块(ngx_http_access_module)

此模块提供了一个简易的基于主机的访问控制。
ngx_http_access_module 模块让我们可以对特定 IP 客户端进行控制。 规则检查按照第一次匹配的顺序,此模块对网络地址有放行和禁止的权利。

1
2
3
4
5
6
7
# 仅允许网段 10.1.1.0/16 和 192.168.1.0/24 中除 192.168.1.1 之外的 ip 访问
location / {
: deny 192.168.1.1;
: allow 192.168.1.0/24;
: allow 10.1.1.0/16;
: deny all;
}
  1. 放行语法
    1
    allow [ address | CIDR | all ]
    作用域: http, server, location, limit_except
    allow 描述的网络地址有权直接访问
  2. 禁止语法
    1
    deny [ address | CIDR | all ]
    作用域: http, server, location, limit_except
    deny 描述的网络地址拒绝访问

Rewrite模块(ngx_http_rewrite_module)

执行 URL 重定向,允许你去掉带有恶意的 URL,包含多个参数(修改).利用正则的匹配,分组和引用,达到目的 配置范例:该模块允许使用正则表达式改变 URL,并且根据变量来转向以及选择配置

  1. if语法
    1
    if (condition) { ... }
    作用域: server, location
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    if ($http_user_agent ~ MSIE) {
    : rewrite ^(.*)$ /msie/$1 break;
    }
    if ($http_cookie ~* "id=([^;] +)(?:;|$)" ) {
    : set $id $1;
    }
    if ($request_method = POST ) {
    : return 405;
    }
    if (!-f $request_filename) {
    : break;
    : proxy_pass http://127.0.0.1;
    }
    if ($slow) {
    : limit_rate 10k;
    }
    if ($invalid_referer) {
    : return 403;
    }
  2. return语法
    这个指令根据规则的执行情况,返回一个状态值给客户端。可使用值包括:204,400,402-406,408,410,411,413,416 及 500-504。也可以发送非标准的 444 代码-未发送任何头信息下结束连接。
    1
    return cod
    作用域: server, location, if
  3. rewrite语法
    这个指令根据正则表达式或者待替换的字符串来更改 URL。指令根据配置文件中的先后顺序执行生效。
    1
    rewrite regex replacement flag
    flag可以有以下取值:
    • last :表示完成 rewrite
    • break:本规则匹配完成后,终止匹配,不再匹配后面的规则
    • redirect:返回 302 临时重定向,地址栏会显示跳转后的地址
    • permanent:返回 301 永久重定向,地址栏会显示跳转后的地址
      作用域: server, location, if

Proxy模块(ngx_http_proxy_module)

此模块能代理请求到其它服务器.也就是说允许你把客户端的 HTTP 请求转到后端服务器(这部分的指令非常多,但不是全部都会被用到,详细指令列表可以上官网查看,这里是比较常见的指令简介)

1
2
3
4
5
6
7
8
# 强制一些被忽略的头传递到客户端
proxy_pass_header Server;
# 允许改写出现在 HTTP 头却被后端服务器触发重定向的 URL,对响应本身不做任何处理
proxy_redirect off;
# 允许你重新定义代理 header 值再转到后端服务器,目标服务器可以看到客户端的原始主机名
proxy_set_header Host $http_host;
# 目标服务器可以看到客户端的真实 ip,而不是转发服务器的 ip
proxy_set_header X-Real-IP $remote_addr;

upstream模块(ngx_http_upstream_module)

该指令将来自客户端的一个请求分到多个上行服务器上,即我们常说的负载均衡。
默认情况下采用轮询策略,每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
可以使用weight来指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

1
2
3
4
5
6
7
8
9
10
upstream backend {
server backend1.example.com weight=5;
server backend2.example.com:8080;
server unix:/tmp/backend3;
}
server {
location / {
proxy_pass http://backend;
}
}
  1. upstream指令
    这个指令描述了一个服务器的集合,该集合可被用于 proxy_pass 和 fastcgi_pass 指令中,作为一个单独的实体。这些服务器可以是监听在不同的端口,另外,并发使用同时监听 TCP 端口和 Unix 套接字的服务器是可能的。 这些服务器能被分配不同的权重。如果没有指定,则都为 1 ,即默认的策略为轮询。
    1
    2
    3
    4
    5
    upstream backend {
    server backend1.example.com weight=5;
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server unix:/tmp/backend3;
    }
    可以通过weight参数指定每个上行服务器的权重,weight和访问比率成正比,用于后端服务器性能不均的情况。
    1
    2
    3
    4
    upstream test{
    server localhost:8080 weight=9;
    server localhost:8081 weight=1;
    }
  2. ip_hash指令
    请求基于客户端的 IP 地址在服务器间进行分发。 IPv4 地址的前三个字节或者 IPv6 的整个地址,会被用来作为一个散列 key。 这种方法可以确保从同一个客户端过来的请求,会被传给同一台服务器。除了当服务器被认为不可用的时候,这些客户端的请求会被传给其他服务器,而且很有可能也是同一台服务器。
    如果其中一个服务器想暂时移除,应该加上 down 参数。这样可以保留当前客户端 IP 地址散列分布。
    作用域:upstream
    1
    2
    3
    4
    5
    6
    7
    upstream backend {
    ip_hash;
    server backend1.example.com;
    server backend2.example.com;
    server backend3.example.com down;
    server backend4.example.com;
    }
  3. fair(第三方)
    按后端服务器的响应时间来分配请求,响应时间短的优先分配。
    1
    2
    3
    4
    5
    upstream backend { 
    fair;
    server localhost:8080;
    server localhost:8081;
    }
  4. url_hash(第三方)
    按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。 在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法。
    1
    2
    3
    4
    5
    6
    upstream backend {
    hash $request_uri;
    hash_method crc32;
    server localhost:8080;
    server localhost:8081;
    }
  5. server指令
    定义服务器的地址 address 和其他参数 parameters。 地址可以是域名或者 IP 地址,端口是可选的,或者是指定“unix:”前缀的 UNIX 域套接字的路径。如果没有指定端口,就使用 80 端口。 如果一个域名解析到多个 IP,本质上是定义了多个 server。
    作用域:upstream
    1
    2
    3
    4
    5
    upstream backend {
    server backend1.example.com weight=5;
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server unix:/tmp/backend3;
    }

nginx配置文件优化

配置文件

默认配置文件位置在/usr/local/nginx/conf/nginx.conf,在configue时决定,也可以在运行nginx时指定配置文件

1
nginx nginx.conf

顶层配置

顶层配置即nginx.conf中前面、暴露在外面的那几项

1
2
3
#user  nobody;
worker_processes 1;
#pid logs/nginx.pid;

worker_processes 定义了 nginx 在为你的网站提供服务时,worker 进程的数量。
这个优化值受到包括 CPU 内核数、存储数据的磁盘数、负载值在内的许多因素的影响。如果不确定的话,将其设置为可用的 CPU 内核的数量是一个不错的选择(设置为“auto”,将会尝试自动检测可用的值)。
另外本机的CPU核心信息可以使用下面命令查看

1
cat /proc/cpuinfo | grep processor

events模块

events模块包括了 nginx 中处理链接的全部设置

1
2
3
4
events {
worker_connections 1024;
# multi_accept on;
}

worker_connections 设置了一个 worker 进程可以同时打开的链接数。
multi_accept 的作用是告诉 nginx 在收到新链接的请求通知时,尽可能接受链接。最好开着

http模块

当外部有 http 请求时,nginx 的 http 模块才是处理这个请求的核心。

  1. Basic Settings
    sendfile 指向 sendfile()函数。sendfile() 在磁盘和 TCP 端口(或者任意两个文件描述符)之间复制数据。
    在 sendfile 出现之前,为了传输这样的数据,需要在用户空间上分配一块数据缓存,使用 read() 从源文件读取数据到缓存,然后使用 write() 将缓存写入到网络。
    sendfile() 直接从磁盘上读取数据到操作系统缓冲。由于这个操作是在内核中完成的,sendfile() 比 read() 和 write() 联合使用要更加有效率。
    tcp_nopush 配置 nginx 在一个数据包中发送全部的头文件,而不是一个一个发送。
    tcp_nodelay 配置 nginx 不要缓存数据,应该快速的发送小数据——这仅仅应该用于频繁发送小的碎片信息而无需立刻获取响应的、需要实时传递数据的应用中。
    keepalive_timeout 指定了与客户端的 keep-alive 链接的超时时间。服务器会在这个时间后关闭链接。我们可以降低这个值,以避免让 worker 过长时间的忙碌。
  2. Logging Settings
    access_log 确定了 nginx 是否保存访问日志。将这个设置为关闭可以降低磁盘 IO 而提升速度。
    error_log 设置 nginx 应当记录错误日志。
  3. Gzip Settings
    gzip 设置 nginx gzip 压缩发送的数据。这会减少需要发送的数据的数量。
    gzip_disable 为指定的客户端禁用 gzip 功能。
    gzip_proxied 允许或禁止基于请求、响应的压缩。设置为 any,就可以 gzip 所有的请求。
    gzip_comp_level 设置了数据压缩的等级。等级可以是 1-9 的任意一个值,9 表示最慢但是最高比例的压缩。
    gzip_types 设置进行 gzip 的类型。有下面这些,不过还可以添加更多。

mail模块

略…

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
user nginxuser;
worker_processes 4;
pid logs/nginx.pid;

events {
worker_connections 1024;
multi_accept on;
}

http {

##
# Basic Settings
##


sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 2048;
# server_tokens off;


# server_names_hash_bucket_size 64;
# server_name_in_redirect off;


include /etc/nginx/mime.types;
default_type application/octet-stream;


##
# Logging Settings
##


access_log off;
error_log /var/log/nginx/error.log;


##
# Gzip Settings
##


gzip on;
gzip_disable "msie6";


gzip_vary on;
gzip_proxied any;
gzip_comp_level 9;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;


##
# nginx-naxsi config
##
# Uncomment it if you installed nginx-naxsi
##


#include /etc/nginx/naxsi_core.rules;


##
# nginx-passenger config
##
# Uncomment it if you installed nginx-passenger
##


#passenger_root /usr;
#passenger_ruby /usr/bin/ruby;


##
# Virtual Host Configs
##


include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}



# mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP4rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}

优化 - Linux 最大连接数

查看 Linux 连接数

默认配置下,Linux 只支持有限的连接数。
Linux 的线程其实是一个进程,所以 java 的也是,具体来说,叫做“light weight process(LWP)”—轻量级进程。
LWP 与其它进程共享所有(或大部分)逻辑地址空间和系统资源,一个进程可以创建多个 LWP,这样它们共享大部分资源;LWP 有它自己的进程标识符,并和其他进程有着父子关系;。LWP 由内核管理并像普通进程一样被调度
使用以下命令可以看到某个用户使用了多少进程资源

1
ps -eLf | grep xjjbot(uid)  | wc -l

使用下面命令可以查看具体每个进程开启了多少线程

1
ps -o nlwp,pid,lwp,args -u xjjbot(uid)  | sort -n

根据 linux 一切都是文件的规则,首先想到的,是修改 ulimit 的参数,然而也不是,因为它已经足够大了。交叉回想一下 elasticsearch,在安装的时候,需要配置一个叫做 nproc 的东西,问题大概就出在这,是进程资源不够用啦。
相关的配置文件:

1
/etc/security/limits.conf

在不同的内核版本上,也有一些小差异。比如:/etc/security/limits.d/*
下的文件,会在某些时候覆盖 limits.conf 的配置。所以配置不生效的情况下,记得检查一下。
鉴于以上原因,可以将 limits.d 中的配置全部注释掉,统一在 limits.conf 中配置。
以下是原始配置

1
2
*          soft    nproc     4096
root soft nproc unlimited

将 4096 改为大点的数字,或者直接改成 unlimited 就可以了。

单机支持 100 万连接是可行的,但带宽问题会成为显著的瓶颈。启用压缩的二进制协议会节省部分带宽,但开发难度增加。

操作系统优化

更改进程最大文件句柄数

1
ulimit -n 1048576

复制代码修改单个进程可分配的最大文件数

1
echo 2097152 > /proc/sys/fs/nr_open

复制代码修改/etc/security/limits.conf 文件

1
2
3
4
*   soft nofile  1048576
* hard nofile 1048576
* soft nproc unlimited
root soft nproc unlimited

复制代码记得清理掉/etc/security/limits.d/*下的配置

网络优化

打开/etc/sysctl.conf,添加配置然后执行,使用 sysctl 生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#单个进程可分配的最大文件数
fs.nr_open=2097152

#系统最大文件句柄数
fs.file-max = 1048576

#backlog 设置
net.core.somaxconn=32768
net.ipv4.tcp_max_syn_backlog=16384
net.core.netdev_max_backlog=16384

#可用知名端口范围配置
net.ipv4.ip_local_port_range='1000 65535'

#TCP Socket 读写 Buffer 设置
net.core.rmem_default=262144
net.core.wmem_default=262144
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.core.optmem_max=16777216
net.ipv4.tcp_rmem='1024 4096 16777216'
net.ipv4.tcp_wmem='1024 4096 16777216'

#TCP 连接追踪设置
net.nf_conntrack_max=1000000
net.netfilter.nf_conntrack_max=1000000
net.netfilter.nf_conntrack_tcp_timeout_time_wait=30

#TIME-WAIT Socket 最大数量、回收与重用设置
net.ipv4.tcp_max_tw_buckets=1048576

# FIN-WAIT-2 Socket 超时设置
net.ipv4.tcp_fin_timeout = 15

参考

  1. Linux Web 运维(Nginx)实战
  2. Nginx 开发从入门到精通
  3. nginx documentation

config

nginx.conf配置文件详解 http://www.ha97.com/5194.html
更多配置技巧 https://www.nginx.com/resources/wiki/start/

Nginx 原理

  1. Nginx 内部有使用多线程吗?
  2. 如果这篇文章说不清 epoll 的本质,那就过来掐死我吧! (1)

模块

  1. log
    Module ngx_http_log_module
    Module ngx_stream_log_module
    nginx 日志格式及自定义日志配置
  2. proxy
    Module ngx_http_proxy_module
  3. TCP / UDP
    How nginx processes a TCP/UDP session

Tengine

  1. Documentation

这一篇是对在公司内缓存代码应用 Redis-Lua 的一个总结,经过 benchmark 测试,这种方式效率更高,且理论上有更低的可能性。
顺便,一开始先描述一下Redis中的事务的原理,因为Redis-Lua本身是事务的一个替代品,这二者一般放在一起讨论。

阅读全文 »

Java5

语法、集合框架等

  • 泛型
  • 枚举
  • 装箱拆箱
  • 变长参数
  • 注解
  • foreach 循环
  • 静态导入
  • 格式化
  • 线程框架/数据结构
  • Arrays 工具类/StringBuilder/instrument

泛型

不同于 C++中的泛型,Java 的泛型会在编译后被清除,这种机制被称为泛型擦除。

java 的类型推断基本都在编译期完成

优点:可以免去大量的显式类型转换;
缺点:由于泛型擦除的存在,在很多场合下容易引起误会:

  • 比如向 List类型的表里添加一个 String 类型对象就不会通过,因为在编译期间还需要进行类型检查。
  • 在继承重写方法时,若父类中被重写的方法中含有泛型,因为泛型擦除理应变成重载,但是 Java 编译器会在编译后的字节码中添加桥方法(已经被类型擦除)、桥方法再调用重写的方法来解决;
  • 泛型类型参数不能使用基本类型,因为基本类型不是 Object 的子类;
  • 其他一些注意事项…

枚举

枚举可以使用 enum 声明,在 switch 中可以作为 case 后的标签。可以使用 EnumMap 来保存枚举到其他类型的映射或使用 EnumSet 保存枚举值的集合。
优点

  • 相对使用 int 或 String 当作枚举对象来说,Java 编译器本身提供了对 enum 的类型检查,可以更安全地使用;
  • 可以用于声明单例对象。

装箱拆箱

基本类型可以自动转换成对应的包装类型,比如 boolean 会被包装为 Boolean。
优点

  • 方便。

缺点

  • 注意拆箱时不能对 null 拆箱,不然会报空指针。

变长参数

可以传入任意多个相同类型的参数。
优点

  • 提供了更多灵活性,比如编写输出方法时可以格式化多个参数。

缺点

  • 需要注意 null 值的传入。

注解

注解需要和反射配合使用,JDK 提供了一些具有特定语义的注解:
@Inherited:是否对类的子类继承的方法等起作用;
@Target:作用目标;
@Rentation:表示 annotation 是否保留在编译过的 class 文件中还是在运行时可读。

for/in 循环

优点

  • 方便

缺点

  • 获取不到元素所在 index;
  • 无法在遍历的时候删除元素;

静态 import

可以直接使用一个类中的静态方法。
缺点

  • 如果有同名的容易引起混淆;

Formatter

提供对日期、数字等的格式化支持

线程框架/数据结构

  1. 在线程中可以设置 UncaughtExceptionHandler,当抛出异常后可以执行指定的逻辑;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    public class ThreadingTest extends Thread {
    private int[] numbers;
    public ThreadingTest(int[] numbers) {
    setName("Simple Thread");
    setUncaughtExceptionHandler(
    new SimpleThreadExceptionHandler());
    this.numbers = numbers;
    }
    public void run() {
    int index = numbers.length;
    boolean finished = false;
    while (!finished) {
    index--;
    finished = true;
    for (int i = 0; i < index; i++) {
    // Create error condition
    if (numbers[i + 1] < 0) {
    throw new IllegalArgumentException(
    "Cannot pass negative numbers into this thread!");
    }
    if (numbers[i] > numbers[i + 1]) {
    // swap
    int temp = numbers[i];
    numbers[i] = numbers[i + 1];
    numbers[i + 1] = temp;
    finished = false;
    }
    }
    }
    }
    public static void main(String[] args) {
    int[] numbers = new int[]{2, -1, 56, 4, 7};
    ThreadingTest threadingTest = new ThreadingTest(numbers);
    threadingTest.start();
    }
    }
    class SimpleThreadExceptionHandler implements
    Thread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
    System.err.printf("%s: %s at line %d of %s%n",
    t.getName(),
    e.toString(),
    e.getStackTrace()[0].getLineNumber(),
    e.getStackTrace()[0].getFileName());
    }
    }
  2. 引入 Queue、BlockingQueue、ConcurrentMap 数据结构;
  3. 引入 JUC 线程池;
    • 每次提交任务时,如果线程数还没达到 coreSize 就创建新线程并绑定该任务。 所以第 coreSize 次提交任务后线程总数必达到 coreSize,不会重用之前的空闲线程。
    • 线程数达到 coreSize 后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用 take()从工作队列里拉活来干。
    • 如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。
    • 临时线程使用 poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。
    • 如果 core 线程数+临时线程数 >maxSize,则不能再创建新的临时线程了,转头执行 RejectExecutionHanlder。默认的 AbortPolicy 抛 RejectedExecutionException 异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务(DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。
  4. Arrays
    提供数组相关的一些工具类。
    1
    2
    3
    4
    5
    Arrays.sort(myArray);
    Arrays.toString(myArray)
    Arrays.binarySearch(myArray, 98)
    Arrays.deepToString(ticTacToe)
    Arrays.deepEquals(ticTacToe, ticTacToe3)

Override 支持协变

返回类型可以是父类中相应类型或其子类。

JVM

CDS(Class Data Sharing)

JRE installer 能将一些系统 jar 文件加载到一种私有内部表示方式,然后转储到一个文件内,称为“shared archive”,下次启动应用的时候可以直接使用这个包内的类数据,这样可以减少部分启动时间。

自动检测服务器级机器

如果机器至少有 2 CPUs 和至少 2GB 物理内存,use the Java HotSpot Server Virtual Machine (server VM) instead of the Java HotSpot Client Virtual Machine (client VM).,The aim is to improve performance even if no one configures the VM to reflect the application it’s running. In general, the server VM starts up more slowly than the client VM, but over time runs more quickly.

垃圾收集器自适应

服务器类机器默认垃圾回收器改为并行垃圾回收器。
可以指定性能目标,并行收集器可以自动调整堆的大小,比如:

1
2
3
4
-XX:GCTimeLimit=time-limit :花费在GC上的时间上限,默认是98,当超过上限时,会抛出OutOfMemory(HeapSpace)的异常
-XX:GCHeapFreeLimit=space-limit :Heap空闲空间的最低比例下限,默认是2,当超过下限时,会抛出OutOfMemory(HeapSpace)的异常
-XX:MaxGCPauseMillis=nnn :最长的GC暂停时间,如果时间过长,会相应调整空间的大小(单位是毫秒)
-XX:GCTimeRatio=nnn :最大的GC占总可用时间的比例,如果时间过长,会相应调整空间的大小(花费在GC上的时间比例不超过1 / (1 + nnn))

线程优先级

Thread 类中给出了三个线程优先级常量:

1
2
3
java.lang.Thread.MIN_PRIORITY = 1
java.lang.Thread.NORM_PRIORITY = 5
java.lang.Thread.MAX_PRIORITY = 10

默认情况下线程优先级为 java.lang.Thread.NORM_PRIORITY,我们可以自定义设置在[1..10]内。
JVM(Java HotSpot)将 Java 线程关联到唯一的一个 native thread。

网络编程(Socket)

InetAddress

IP 地址是在网络层封装上的,确定 Internet 上的一个唯一的地址,端口号是由传输层封装上的,标志主机上的一个服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InetAddressTest {
public static void main(String args[]){
InetAddressTest.printAddress();
}
static void printAddress(){
try{
InetAddress address = InetAddress.getLocalHost();
System.out.println(address);//输出:机器名/IP地址
address = InetAddress.getByName("www.baidu.com");
System.out.println(address);//输出:域名/IP地址
InetAddress[] addresses = InetAddress.getAllByName("www.baidu.com");
for(InetAddress a : addresses){
System.out.println(a);
}
}
catch(UnknownHostException e){
e.printStackTrace();
}
}
}

URL 和 UrlConnection

UrlConnection 可以从一个 URL 中打开流,可以方便地进行 Http 数据的收发。
内部是使用 Socket 进行连接的。

1
2
3
4
5
6
7
8
9
// 获取链接属性
URL url = new URL("http://java.sun.com:80/docs/books/tutorial/index.html#DOWN");//#后面的DOWN是位置标识符,在获得网页后,浏览器将直接跳到网页的DOWN处读取
String protocal = url.getProtocol();
String host = url.getHost();
String file = url.getFile();
int port = url.getPort();
String ref = url.getRef();//获得#后面的
System.out.println(protocal + ", " + host + ", " + file + ", "
+ port + ", " + ref);

访问链接

1
2
3
4
5
6
7
8
9
10
// 访问链接读取数据
URL url = new URL("http://www.cnblogs.com/mengdd/archive/2013/03/09/2951877.html");
BufferedReader reader = new BufferedReader(
new InputStreamReader(url.openStream()));

String line;
while((line = reader.readLine()) != null){
System.out.println(line);
}
reader.close();

下面是对 URLConnection 的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class URLTest {
public static void main(String args[]){
URLTest c = new URLTest();
c.createURL();
c.printURLParam();
c.readURL();
c.printURL();
}
void createURL(){
try{
URL url = new URL("http://localhost:8080/");
}
catch(MalformedURLException e){
e.printStackTrace();
}
}
void printURLParam(){
try{
URL url = new URL("http://java.sun.com:80/docs/books/tutorial/index.html#DOWN");//#后面的DOWN是位置标识符,在获得网页后,浏览器将直接跳到网页的DOWN处读取
String protocal = url.getProtocol();
String host = url.getHost();
String file = url.getFile();
int port = url.getPort();
String ref = url.getRef();//获得#后面的
System.out.println(protocal + ", " + host + ", " + file + ", "
+ port + ", " + ref);
}
catch(MalformedURLException e){
e.printStackTrace();
}
}
void readURL(){//将网页内容拷贝到本地
try{
URL url = new URL("http://www.baidu.com");
URLConnection conn = url.openConnection();
InputStream is = conn.getInputStream();
//或者直接is = url.openStream();
OutputStream os = new FileOutputStream("e:\\baidu.txt");
byte[] buffer = new byte[2048];
int length = 0;
while((length = is.read(buffer, 0, buffer.length)) != -1){
os.write(buffer, 0, length);
}
is.close();
os.close();
}
catch(MalformedURLException e){
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}
}
void printURL(){//读取网页内容到控制台
try{
URL url = new URL("http://www.baidu.com");
BufferedReader reader = new BufferedReader(
new InputStreamReader(url.openStream()));
String line;
while((line = reader.readLine()) != null){
System.out.println(line);
}
reader.close();
}
catch(MalformedURLException e){
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}
}
}

TCP 和 UDP

它们都是位于传输层的协议,为应用进程提供服务,根据不同的应用场景,会使用不同的协议。
TCP 是基于连接的、面向流的协议,提供可靠通信,因此每次通信必须先建立连接,建立连接后可以分多次进行传输任务,并且保证数据的正确性。
UDP 是基于无连接的、面向数据报的协议,提供不可靠通信,每次通信只需要发送一次数据报,可以分多次发送,但不保证能否到达、到达的顺序。

Socket 是 TCP 的应用编程接口,DatagramSocket 是 UDP 的应用编程接口,他们之间没有继承关系(都实现 Closeable 接口)。
Socket 使用时需要先指定目标主机地址和端口号,然后打开 io 流进行操作
1.服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 监听8080端口
ServerSocket server = new ServerSocket(8080);
// 等待请求
Socket socket = server.accept();
// 进行通信
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String line = reader.readLine();
System.out.println(line);
PrintWriter writer = new PrintWriter(socket.getOutputStream());
writer.println(line);
writer.flush(); // 不要忘了这个
// 关闭资源
writer.close();
reader.close();
socket.close();
server.close();

2.客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 监听8080端口
Socket socket = new Socket("127.0.0.1", 8080);
// 开始通信
PrintWriter writer = new PrintWriter(socket.getOutputStream());
writer.println("hello");
writer.flush();
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String line = reader.readLine();
System.out.println(line);
// 关闭资源
writer.close();
reader.close();
socket.close();

SocketChannel 和 ServerSocketChannel

缓冲(Buffer):相当于货物
管道(Channel):相当于配货车,支持同时装多件货物。
选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况。相当于中转站的分拣员。

使用 DatagramSocket 进行 UDP 通信

下面的代码使用 DatagramSocket 实现 UDP 通信

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建服务器socket,指定端口
DatagramSocket socket = new DatagramSocket(7000);
// 接收信息保存到一个缓冲区,DatagramPacket(bytes, len)
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 1024);
socket.receive(packet);
System.out.println(new String(buffer, 0, packet.getLength()));
// 响应信息,接收时需要指定地址和端口
String str = "Welcome!";
DatagramPacket packet1 = new DatagramPacket(str.getBytes(),
str.length(), packet.getAddress(), packet.getPort());
socket.send(packet1);
// 关闭资源
socket.close();

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
DatagramSocket socket = new DatagramSocket();
// 发送数据包
String str = "Hello World";
DatagramPacket packet = new DatagramPacket(str.getBytes(),
str.length(), InetAddress.getByName("localhost"), 7000);
socket.send(packet);
// 接收响应
byte[] buffer = new byte[1024];
DatagramPacket packet1 = new DatagramPacket(buffer, 100);
socket.receive(packet1);
System.out.println(new String(buffer, 0, packet1.getLength()));
// 关闭资源
socket.close();

下面的代码使用 NIO 实现数据报协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 发送
public void send() throws IOException{
// 打开数据报通道
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + ":\n" + str).getBytes());
buf.flip();
dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
buf.clear();
}
dc.close();
}
// 接收
public void receive() throws IOException{
// 传送数据报通道
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
dc.register(selector, SelectionKey.OP_READ);
while(selector.select() > 0){
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
SelectionKey sk = it.next();
if(sk.isReadable()){
ByteBuffer buf = ByteBuffer.allocate(1024);
dc.receive(buf);
buf.flip();
System.out.println(new String(buf.array(), 0, buf.limit()));
buf.clear();
}
}
it.remove();
}
}

使用 NIO 实现简易 HttpServer

主要思路很简单:
(1) 服务器打开后首先为 Selector 注册一个 OP_ACCEPT 的 key,这样 select 时就能接收客户端请求了;
(2) 每接收一个请求后即为该 key 创建一个线程,处理该 key 的操作,操作包括 accept 和 read,对于前者,只需为该 key 的 selector 再注册一个 OP_READ 用于准备接下来的读请求;
(3) 读取时先读入一个 Buffer,首先解析请求头部分,直到遇到一个空行结束,因为这里只考虑 GET 请求,所以不必继续解析请求体了;
(4) 返回时,首先构建响应头,同样使用一个空行结束,然后构建响应体,写回客户端,结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public class HttpServer {
public static void main(String[] args) throws IOException {
// 监听8080端口
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(8080));
// 设置为非阻塞模式
server.configureBlocking(false);
// 为server注册选择器
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
// 创建处理器
while(true) {
// 等待请求,每次阻塞3s,若超过3s线程继续运行,
// select(0)或select()表示一直阻塞
if(selector.select(3000) == 0) {
continue;
}
// 获取所有待处理的选择键
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
while(keyIter.hasNext()) {
SelectionKey key = keyIter.next();
// 启动新线程以处理SelectionKey
new Thread(new HttpHandler(key)).run();
// 处理完毕后,移除当前key
keyIter.remove();
}
}
}

private static class HttpHandler implements Runnable {
private int bufferSize = 1024;
private String localCharset = "UTF-8";
private SelectionKey key;

public HttpHandler(SelectionKey key) {
this.key = key;
}
// 定义操作
private void handleAccept() throws IOException {
// 接受请求后,注册OP_READ选择键以等待下一次请求
SocketChannel clientChannel = ((ServerSocketChannel)key.channel()).accept();
clientChannel.configureBlocking(false);
// !!!请求报文被限制在1024个字节内
clientChannel.register(key.selector(),
SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}
private void handleRead() throws IOException {
// 获取
SocketChannel sc = (SocketChannel) key.channel();
// 获取Buffer并重置
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
// 读取,并判断内容是否为空,若是则关闭并退出
if(sc.read(buffer) == -1) {
sc.close();
return;
}
// 接收请求数据
buffer.flip();
String receivedString = Charset.forName(localCharset).newDecoder().
decode(buffer).toString();

// 打印请求报文头
String[] requestMessage = receivedString.split("\r\n");
for(String s: requestMessage) {
System.out.println(s);
// 遇到空行说明报文头已经打印完
if(s.isEmpty()) {
break;
}
}
// 控制台打印首行信息
String[] firstLine = requestMessage[0].split(" ");
System.out.println();
System.out.println("Method:\t" + firstLine[0]);
System.out.println("url:\t" + firstLine[1]);
System.out.println("HTTP Version:\t" + firstLine[2]);
System.out.println();
// 返回客户端(!!!考虑对不同的Url和不同的请求方法进行不同的处理和响应)
StringBuilder sendString = new StringBuilder();
sendString.append("HTTP/1.1 200 OK\r\n"); // 响应报文首行
sendString.append("Content-Type:text/html;charset=" + // !!!如果要传输流数据必须修改Content-Type
localCharset + "\r\n");
sendString.append("\r\n"); // 报文结束后加一空行
// 响应体
sendString.append("<html><head><title>显示报文</title></head><body>");
sendString.append("接收到请求报文是: <br/>");
for(String s: requestMessage) {
sendString.append(s + "<br/>");
}
sendString.append("</body></html>");
// 使用缓冲区写入channel
buffer = ByteBuffer.wrap(sendString.toString().getBytes(localCharset));
sc.write(buffer);
// 关闭资源
sc.close();
}

public void run() {
try {
// 根据请求类型进行转发
if(key.isAcceptable()) {
handleAccept();
}
if(key.isReadable()) {
handleRead();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

使用 NIO-Selector 实现简易聊天室

客户端

声明数据结构

1
2
3
4
// 管道、选择器、字符集
private SocketChannel sc = null;
private Selector selector = null;
private Charset charset = Charset.forName("UTF-8");

创建线程类用于从服务端获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private class ClientThread extends Thread {
public void run() {
try {
// 遍历所有选择键
while(selector.select() > 0) {
for(SelectionKey sk : selector.selectedKeys()) {
// 删除正在处理的
selector.selectedKeys().remove(sk);
// 如果该键对应的通道中有可读的数据
if(sk.isReadable()) {
// 使用缓冲区读取管道内的数据
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
while(sc.read(buff) > 0) {
buff.flip();
content += charset.decode(buff);
}
// 打印
System.out.println("聊天信息" + content);
sk.interestOps(SelectionKey.OP_READ);
}
}
}
} catch(IOException e) {
e.printStackTrace();
}
}
}

初始化

1
2
3
4
5
6
7
// 初始化SocketChannel
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 9999);
sc = SocketChannel.open(isa);
sc.configureBlocking(false);
// 注册选择器
selector = Selector.open();
sc.register(selector, SelectionKey.OP_READ);

创建线程从服务端拉取数据,及不断从键盘读入发送到服务端

1
2
3
4
5
6
7
8
// 启动线程不断从服务端拉取
new ClientThread().start();
// 读取键盘输入到通道
Scanner reader = new Scanner(System.in);
while(reader.hasNextLine()) {
String line = reader.nextLine();
sc.write(charset.encode(line));
}

服务端

声明

1
2
3
// 选择器、字符集
private Selector selector = null;
private Charset charset = Charset.forName("UTF-8");

初始化

1
2
3
4
5
6
7
8
// 打开管道
ServerSocketChannel server = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 9999);
server.socket().bind(isa);
server.configureBlocking(false);
// 打开选择器
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);

接受连接,读取及发送数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 依次处理选择器上的选择键
while(selector.select() > 0) {
for(SelectionKey sk : selector.selectedKeys()) {
selector.selectedKeys().remove(sk);
// 连接请求
if(sk.isAcceptable()) {
SocketChannel sc = server.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
sk.interestOps(SelectionKey.OP_ACCEPT);
}
// 存在可读取数据
if(sk.isReadable()) {
// 使用缓冲区读取
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
try {
while(sc.read(buff) > 0) {
buff.flip();
content += charset.decode(buff);
}
System.out.println("=======" + content);
// 将管道设置为准备下一次读取
sk.interestOps(SelectionKey.OP_READ);
} catch(IOException e) {
// 如果该sk对应的管道出现异常,表明管道的客户端出现异常,
// 所以从选择器中取消sk
e.printStackTrace();
sk.cancel();
if(sk.channel() != null) {
sk.channel().close();
}
}
// 说明聊天信息不为空
if(content.length() > 0) {
// 将聊天信息输入每个选择键对应的管道中
for(SelectionKey key : selector.keys()) {
Channel targetChannel = key.channel();
if(targetChannel instanceof SocketChannel) {
SocketChannel dest = (SocketChannel) targetChannel;
dest.write(charset.encode(content));
}
}
}
}
}
}

QA

  1. 为什么 Socket 可以通过流来“持续地”读写,而 DatagramSocket 却只能一个一个数据报发哩?
    这是由 TCP 和 UDP 的协议决定的,TCP 是面向流的协议,而 UDP 是面向数据报的协议。
  2. 可以用 TCP 客户端连接 UDP 服务器吗(或者反过来)?
    不能,实验过确实不行,但是我还是心存疑惑,我猜测是因为接收方可以判断数据包的协议类型来确定是否接收。
  3. socket 是怎么实现”全双工”的?

参考

  1. Java Code Examples
  2. Java 应用中的日志
  3. 错误处理的推荐实践
  4. Java 开发中 10 个最为微妙的最佳编程实践
  5. 《Effective Java》
  6. 阿里巴巴编码规范(Java)
  7. Java 泛型中的 PECS 原则

JNI

  1. Program Library HOWTO (how to create and use program libraries on Linux)
  2. Java and C/C++: JNI Guide

Java5

  1. Java5 的新特性
  2. New Features and Enhancements J2SE 5.0
  3. java 泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题

网络

  1. HttpClient 使用详解
  2. 基于 JavaMail 的 Java 邮件发送:简单邮件发送
  3. qq 邮箱服务器地址

工程

  1. IntelliJ IDEA 使用教程(2019 图文版) – 从入门到上瘾
0%