目录
1. TCP、UDP
2. Java Tcp
3. Java Upd
4. 降级的几种方式
5. 降级的实现方式之一
6. 故障中降级的意义
7. 总结及展望
1. TCP、UDP
TCP/IP协议栈主要分为四层:应用层、传输层、网络层、数据链路层,每层都有相应的协议,所谓的协议就是双方进行数据传输的一种格式。
应用层:用户进程
传输层:TCP、UDP
网络层:ICMP、IP、IGMP
链路层:ARP、硬件接口、RARP
网络中,一帧以太网数据包的格式如下:
| Ethernet头 | IP头 | TCP/UDP头 | 数据 |
image
在Linux 操作系统中,当我们想发送数据的时候,我们只需要在上层准备好数据,然后提交给内核协议栈 , 内核协议栈自动添加相应的协议头。
1.1. TCP
TCP协议是面向连接、保证高可靠性(数据无丢失、数据无失序、数据无错误、数据无重复到达)传输层协议。
TCP头分析
image
(1)端口号[16bit]
网络实现的是不同主机的进程间通信。在一个操作系统中,有很多进程,当数据到来时要提交给哪个进程进行处理呢?这就需要用到端口号。在TCP头中,有源端口号(Source Port)和目标端口号(Destination Port)。源端口号标识了发送主机的进程,目标端口号标识接受方主机的进程。
(2)序号[32bit]
序号分为发送序号(Sequence Number)和确认序号(Acknowledgment Number)。
发送序号:用来标识从 TCP源端向 TCP目的端发送的数据字节流,它表示在这个报文段中的第一个数据字节的顺序号。如果将字节流看作在两个应用程序间的单向流动,则 TCP用顺序号对每个字节进行计数。序号是 32bit的无符号数,序号到达 2 32- 1后又从 0开始。当建立一个新的连接时, SYN标志变 1,顺序号字段包含由这个主机选择的该连接的初始顺序号 ISN( Initial Sequence Number)。
确认序号:包含发送确认的一端所期望收到的下一个顺序号。因此,确认序号应当是上次已成功收到数据字节顺序号加 1。只有 ACK标志为 1时确认序号字段才有效。 TCP为应用层提供全双工服务,这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据顺序号。
(3)偏移[4bit]
这里的偏移实际指的是TCP首部的长度,它用来表明TCP首部中32 bit字的数目,通过它可以知道一个TCP包它的用户数据是从哪里开始的。这个字段占4bit,如4bit的值是0101,则说明TCP首部长度是5 * 4 = 20字节。 所以TCP的首部长度最大为15 * 4 = 60字节。然而没有可选字段,正常长度为20字节。
(4)Reserved [6bit]
目前没有使用,它的值都为0
(5)标志[6bit]
在TCP首部中有6个标志比特。他们中的多个可同时被置为1 。
URG 紧急指针(urgent pointer)有效
ACK 确认序号有效
PSH 指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满
RST 一般表示断开一个连接
例如:一个TCP的客户端向一个没有监听的端口的服务器端发起连接,wirshark抓包如下
image
可以看到host:192.168.63.134向host:192.168.63.132发起连接请求,但是host:192.168.63.132并没有处于监听对应端口的服务器端,这时
host : 192.168.63.132发一个RST置位的TCP包断开连接。
SYN 同步序号用来发起一个连接
FIN 发送端完成发送任务(即断开连接)
(6)窗口大小(window)[16bit]
窗口的大小,表示源方法最多能接受的字节数。。
(7)校验和[16bit]
校验和覆盖了整个的TCP报文段:TCP首部和TCP数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。
(8)紧急指针[16bit]
只有当URG标志置为1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。
(9)TCP选项
是可选的,在后面抓包的时候,我们在看看它
==TCP 三次握手建立连接==
a.请求端(通常称为客户)发送一个SYN段指明客户打算连接的服务器的端口,以及初始序号(ISN,在这个例子中为1415531521)。这个SYN段为报文段1。
b.服务器发回包含服务器的初始序号的SYN报文段(报文段2)作为应答。同时,将确认序号设置为客户的ISN加1以对客户的SYN报文段进行确认。一个SYN将占用一个序号
c.客户必须将确认序号设置为服务器的ISN加1以对服务器的SYN报文段进行确认(报文段3)
这三个报文段完成连接的建立。这个过程也称为三次握手(three-way handshake)
image
==TCP 四次挥手断开连接==
a.现在的网络通信都是基于socket实现的,当客户端将自己的socket进行关闭时,内核协议栈会向服务器自动发送一个FIN置位的包,请求断开连接。我们称首先发起断开请求的一方称为主动断开方。
b.服务器端收到请客端的FIN断开请求后,内核协议栈会立即发送一个ACK包作为应答,表示已经收到客户端的请求
c.服务器运行一段时间后,关闭了自己的socket。这个时候内核协议栈会向客户端发送一个FIN置位的包,请求断开连接
d.客户端收到服务端发来的FIN断开请求后,会发送一个ACK做出应答,表示已经收到服务端的请求
image
TCP可靠性的保证
TCP采用一种名为“带重传功能的肯定确认(positive acknowledge with retransmission)”的技术作为提供可靠数据传输服务的基础。这项技术要求接收方收到数据之后向源站回送确认信息ACK。发送方对发出的每个分组都保存一份记录,在发送下一个分组之前等待确认信息。发送方还在送出分组的同时启动一个定时器,并在定时器的定时期满而确认信息还没有到达的情况下,重发刚才发出的分组。图3-5表示带重传功能的肯定确认协议传输数据的情况,图3-6表示分组丢失引起超时和重传。为了避免由于网络延迟引起迟到的确认和重复的确认,协议规定在确认信息中稍带一个分组的序号,使接收方能正确将分组与确认关联起来。
从图 3-5可以看出,虽然网络具有同时进行双向通信的能力,但由于在接到前一个分组的确认信息之前必须推迟下一个分组的发送,简单的肯定确认协议浪费了大量宝贵的网络带宽。为此, TCP使用滑动窗口的机制来提高网络吞吐量,同时解决端到端的流量控制。
image
滑动窗口技术
滑动窗口技术是简单的带重传的肯定确认机制的一个更复杂的变形,它允许发送方在等待一个确认信息之前可以发送多个分组。如图 3-7所示,发送方要发送一个分组序列,滑动窗口协议在分组序列中放置一个固定长度的窗口,然后将窗口内的所有分组都发送出去;当发送方收到对窗口内第一个分组的确认信息时,它可以向后滑动并发送下一个分组;随着确认的不断到达,窗口也在不断的向后滑动。
image
### 1.2. UDP
UDP协议
UDP协议也是传输层协议,它是无连接,不保证可靠的传输层协议。它的协议头比较简单
image
2. TCP java实现
image
image
TCPServer
import java.io.*;
import java.net.*;
class TCPServer{
public static void main(String[] args)throws IOException{
ServerSocket listen = new ServerSocket(5050);
Socket server = listen.accept();
InputStream in = server.getInputStream();
OutputStream out = server.getOutputStream();
char c = (char)in.read();
System.out.println("收到:" + c);
out.write('s');
out.close();
in.close();
server.close();
listen.close();
}
}
TCPClient
import java.io.*;
import java.net.*;
class TCPClient{
public static void main(String[] args)throws IOException{
Socket client = new Socket("127.0.0.1" , 5050);
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
out.write('c');
char c = (char)in.read();
System.out.println("收到:" + c);
out.close();
in.close();
client.close();
}
}
3. UDP java实现
UDP和TCP有两个典型的区别,一个就是它不需要建立连接,另外就是它在每次收发的报文都保留了消息的边界。
server端 因为UDP协议不需要建立连接,它的过程如下:
构造DatagramSocket实例,指定本地端口。
通过DatagramSocket实例的receive方法接收DatagramPacket.DatagramPacket中间就包含了通信的内容。
通过DatagramSocket的send和receive方法来收和发DatagramPacket.
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
public class UDPSendTest {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket();
byte[] buffer = "hello xiaobin".getBytes();
DatagramPacket dp = new DatagramPacket(buffer,buffer.length, InetAddress.getByName("127.0.0.1"),7879);
socket.send(dp);
socket.close();
}
}
client端 UDP客户端的步骤也比较简单,主要包括下面3步:
构造DatagramSocket实例。
通过DatagramSocket实例的send和receive方法发送DatagramPacket报文。
结束后,调用DatagramSocket的close方法关闭。
因为和TCP不同,UDP发送报文的时候可以在同一个本地端口随意发送给不同的服务器,一般不需要在UDP的DatagramSocket的构造函数中指定目的服务器的地址。
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UDPReceiveTest {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(7879);
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf,buf.length);
socket.receive(dp);
System.out.println("host : " + dp.getAddress().getHostAddress()+ " : " + dp.getPort() + " : " + dp.getSocketAddress());
System.out.println(new String(dp.getData(),0,dp.getLength()));
socket.close();
}
}
4. 服务降级
对于RPC服务来说,核心是远程调用,服务降级属于服务治理方面的内容。
降级方式
手动降级:服务超时超过一定比例后,手动将服务关闭
自动降级:根据服务请求的通过率,自动处理。通过率 = 成功的请求 / 总请求
服务统计:列表页的服务在请求时,是按key值进行请求不同的方法,底层去掉用不同的服务获取数据。因此根据这个前提条件,可以实现服务的手动和自动降级
手动降级:根据服务管理平台的报警,当调用底层服务发生超时或异常时,判断是否需要进行手动处理,手动屏蔽相关的调用key。
自动降级:在数据返回时,根据是否发生异常判断服务的成功或失败情况,统计服务的通过率,当底层服务发生较多异常时,会自动屏蔽对该key的调用。
降级实现
手动降级,通过控制调用key来访问底层的服务获取数据,key可以通过多种方式传递到服务入口:
通过服务接口
通过zk
通过发送udp命令
其他方式
自动降级,计算服务的通过率可以根据统计的服务成功量和失败量来统计,对于降级策略,需要慎重选择,根据服务的通过率来设置一定比例的控制,在自动降级中实现快速失败,缓慢升级的原则来处理有问题的服务。
5. 降级实现
手动降级:通过增加黑名单监听,在服务进行请求时,先查询屏蔽名单中是否有该服务,根据判断结果进行处理。服务在启动时,同时启动一个监听线程,监听一个端口,当有需要降级的key时,可以通过发送udp命令的方式将key发送过来,启动对该key的降级,服务重启后失效。定义发送的upd命令:list、add、clear
BlackListListener 创建命令接口
public interface BlackListListener {
void addAll(List items);
void removeAll(List items);
void clear();
Set list();
boolean contains(String item);
boolean isEmpty();
}
CommandServer 接收命令的监听方法,BlackListListener设置相关的屏蔽内容,服务启动时初始化该方法,直接贴代码
public class CommandServer extends Thread{
private static final Logger LOG = LoggerFactory.getLogger(CommandServer.class);
private DatagramSocket serverSocket;
volatile boolean running = true;
private BlackListListener black = BlackListenerService.getInstance();
public void serverStart() throws SocketException {
try {
Config config = new Config(Dict.APP_CONFIG_PROPERTIES);
int port = ConfigUtil.getPropertyCount(config, "WORK.COMMAND.SERVER.PORT", 9991);
LOG.info("start a DatagramSocket with port :" + port);
serverSocket = new DatagramSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
}
public void serverStop(){
this.setDaemon(true);
this.running = false;
if (serverSocket != null){
serverSocket.close();
}
}
public void start(){
try {
this.serverStart();
super.start();
} catch (SocketException e) {
e.printStackTrace();
}
}
public void run(){
while (running){
try {
byte[] receiveBuff = new byte[4096];
DatagramPacket dp = new DatagramPacket(receiveBuff, receiveBuff.length);
serverSocket.receive(dp);
if (dp.getLength() == 0){
continue;
}else{
String receiveStr = new String(dp.getData(), "UTF-8");
receiveStr = receiveStr.replace('\n',' ').replace('\r',' ').trim();
LOG.info(String.format("input[%s], source ip:[%s]", receiveStr, dp.getAddress()));
InetAddress addr = dp.getAddress();
int port = dp.getPort();
DatagramPacket dpc = output(addr, port, parser(receiveStr));
serverSocket.send(dpc);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String parser(String cmd){
String backString = "Error command!";
if (StringUtils.isEmpty(cmd)){
return backString + "\r\n";
}
cmd = cmd.trim();
if ("list".equals(cmd)) {
backString = "["+Joiner.on(",").join(black.list())+"]";
} else if (cmd.startsWith("add ")) {
cmd = cmd.replaceFirst("add ", "");
String[] cmdArray = cmd.split("\\s");
black.addAll(Arrays.asList(cmdArray));
backString = "["+Joiner.on(",").join(black.list())+"]";
} else if (cmd.startsWith("remove ")) {
cmd = cmd.replaceFirst("remove ", "");
String[] cmdArray = cmd.split("\\s");
black.removeAll(Arrays.asList(cmdArray));
backString = "["+Joiner.on(",").join(black.list())+"]";
} else if (cmd.equalsIgnoreCase("clear")) {
black.clear();
backString = "["+Joiner.on(",").join(black.list())+"]";
} else if(cmd.startsWith("up ")){
cmd = cmd.replaceFirst("up ", "");
String[] cmdArray = cmd.split("\\s");
if (cmdArray.length==0){
backString = "help:ignore name name";
}else{
for(String name: cmdArray){
CrossRateManager.getCrossRateManager().ignore(name, false);
}
backString = CrossRateManager.getCrossRateManager().list();
}
}else if(cmd.startsWith("down ")) {
cmd = cmd.replaceFirst("down ", "");
String[] cmdArray = cmd.split("\\s");
if (cmdArray.length == 0) {
backString = "help:ignore name name";
} else {
for (String name : cmdArray) {
CrossRateManager.getCrossRateManager().ignore(name, true);
}
backString = CrossRateManager.getCrossRateManager().list();
}
}
backString += "\r\n";
return backString;
}
private DatagramPacket output(InetAddress addr, int port, String text){
DatagramPacket dp = new DatagramPacket(text.getBytes(), text.getBytes().length, addr, port);
return dp;
}
public static void main(String[] args) {
CommandServer cs = new CommandServer();
cs.start();
}
}
6. 降级意义
根据线上生产环境发生故障的经验,我们对服务进行了统计,按服务的重要性进行分级:基础信息、扩展信息,在必要时,可以对两者进行分别降级。
参考: