通信协议

TCP/IP简介

TCP/IP(Transmission Control Protocol/Internet Protocol)协议由网络层的IP协议和传输层的TCP协议组成。
TCP/IP提供点对点的链接机制,将数据应该如何封装、定址、传输、路由以及在目的地如何接收,都加以标准化。它将软件通信过程抽象化为四个抽象层,采取协议堆栈的方式,分别实现出不同通信协议。协议族下的各种协议,依其功能不同,被分别归属到这四个层次结构之中,常被视为是简化的七层OSI模型。
通俗而言:TCP负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地,而IP是给因特网的每一台联网设备规定一个地址。

TCP/IP参考模型

TCP建立连接为什么是三次握手?

在谢希仁著《计算机网络》第四版中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。书的例子是这样的:
“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络节点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段,但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”

TCP three-way handshake

在Google Groups的TopLanguage中看到一帖讨论TCP“三次握手”觉得很有意思。贴主提出“TCP建立连接为什么是三次握手?”的问题,在众多回复中,有一条回复写道:“这个问题的本质是,信道不可靠,但是通信双方需要就某个问题达成一致。而要解决这个问题,无论你在消息中包含什么信息,三次通信是理论上的最小值。所以三次握手不是TCP本身的要求,而是为了满足“在不可靠信道上可靠地传输信息”这一需求所导致的。请注意这里的本质需求:信道不可靠,数据传输要可靠。三次达到了,那后面你想接着握手也好,发数据也好,跟进行可靠信息传输的需求就没关系了。因此,如果信道是可靠的,即无论什么时候发出消息,对方一定能收到,或者你不关心是否要保证对方收到你的消息,那就能像UDP那样直接发送消息就可以了”。这可视为对“三次握手”目的的另一种解答思路。

TCP three-way handshake

下面我们通过 Wireshark 工具分析三次握手过程:
打开 Wireshark 软件,在浏览器中访问 http://www.12306.cn/mormhweb/ 网站,在 Wireshark 中输入http过滤,然后选中 GET /mormhweb/ HTTP/1.1 那条记录,右键选择 Follow->TCP Stream 就能看到浏览器打开网站相关的数据包了,如下图所示:

TCP three-way handshake analysis

第一次握手:客户端发送连接请求报文段,标志位为SYN(Synchronize Sequence Numbers是建立连接时使用的握手信号),序列号Seq为0,然后,客户端进入SYN_SEND状态,等待服务器的确认;

第二次握手:服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,将确认序号ACK(Acknowledgement Number)设置为Seq+1,同时自己也发送一个SYN包,即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK,此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

TCP释放连接为什么是四次挥手?

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭:

  1. 第一次挥手:当客户端发送断开连接的请求(即FIN报文)给服务端时,仅仅代表客户端不会再发送数据报文了,但仍可以接收数据报文,此时客户端处于FIN_WAIT_1状态。

  2. 第二次挥手:服务端收到了客户端的FIN报文,然后发送一个ACK确认(这样客户端便不会因为没有收到应答而继续发送断开连接的请求),表明自己接收到了客户端关闭连接的请求,但是还没准备好关闭(可能还有相应的数据报文需要发送)。服务端发送完毕之后,进入CLOSE_WAIT状态,客户端收到这个包的时候进入FIN_WAIT_2状态。

  3. 第三次挥手:当服务端处理完数据报文准备好关闭连接的时候,向客户端发送FIN报文,表明自己准备好关闭连接了,这样可以保证数据通信正常可靠地完成。发送完毕之后服务端进入LASK_ACK状态,等待客户端的最后一个ACK确认。

  4. 第四次挥手:客户端接收到了服务端的的关闭请求之后,发送最后一个ACK确认包,然后进入到TIME_WAIT状态,等待可能出现的ACK包。服务器收到最后一个ACK之后,确认关闭连接,进入CLOSE状态。客户端在等待了某个固定时间之后,没有收到服务端的ACK,就认为服务端已经正常关闭连接了,于是也将自己的连接关闭了,进入CLOSE状态。

通信方式

单工通信:消息只能单方向传输的工作方式(如遥控、广播)。
半双工通信:可以实现双向的通信,但不能在两个方向上同时进行,必须轮流交替地进行(如对讲机)。
全双工通信:通信的双方可以同时发送和接收信息的信息交互方式(如电话通信)。

通讯模式

单播(Unicast):指数据包在计算器网络的传输中,目的地址为单一目标的一种传输方式。它是现今网络应用最为广泛,通常所使用的网络协议或服务大多采用单播传输,例如基于TCP的协议。
组播(Multicast):指把信息同时传递给一组目的地址。组播的应用主要在于实时流量,如在线视频、炒股等,对时间敏感要求强(REAL TIME)。若基于TCP,丢包会重传,若是看电影,画面会出现倒退,因为重传了,若是基于UDP,则直接跳过了。
广播(Broadcast):指将信息数据包发往指定网络范围内的所有设备。

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
public class MulticastSocketServer {

final static String INET_ADDR = "224.0.0.3";
final static int PORT = 8888;

public static void main(String[] args) throws UnknownHostException, InterruptedException {
// Get the address that we are going to connect to.
InetAddress addr = InetAddress.getByName(INET_ADDR);

// Open a new DatagramSocket, which will be used to send the data.
try (DatagramSocket serverSocket = new MulticastSocket()) {
for (int i = 0; i < 10; i++) {
String msg = "Sent message no " + i;

// Create a packet that will contain the data
// (in the form of bytes) and send it.
DatagramPacket msgPacket = new DatagramPacket(msg.getBytes(),
msg.getBytes().length, addr, PORT);
serverSocket.send(msgPacket);

System.out.println("Server sent packet with msg: " + msg);
TimeUnit.SECONDS.sleep(2);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

public class MulticastSocketClient {

final static String INET_ADDR = "224.0.0.3";
final static int PORT = 8888;

public static void main(String[] args) throws UnknownHostException {
// Get the address that we are going to connect to.
InetAddress address = InetAddress.getByName(INET_ADDR);

// Create a buffer of bytes, which will be used to store
// the incoming bytes containing the information from the server.
// Since the message is small here, 256 bytes should be enough.
byte[] buf = new byte[256];

// Create a new Multicast socket (that will allow other sockets/programs
// to join it as well.
try (MulticastSocket clientSocket = new MulticastSocket(PORT)) {
//Joint the Multicast group.
clientSocket.joinGroup(address);

while (true) {
DatagramPacket msgPacket = new DatagramPacket(buf, buf.length);
clientSocket.receive(msgPacket);

String msg = new String(buf, 0, buf.length);
System.out.println("Socket 1 received msg: " + msg);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

I/O模型

BIO(同步阻塞):用户进程发起一个IO操作以后,必须等待IO操作真正完成后,才能继续运行。

NIO(同步非阻塞):用户进程发起一个IO操作以后,可以去做其它事情,但用户进程需要经常询问IO操作是否完成,这样造成不必要的CPU资源浪费。

AIO(异步非阻塞):用户进程发起一个IO操作然后,立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知。

同步和异步指的是对于消息结果的获取是客户端主动获取还是由服务端推送的,而阻塞和非阻塞指的是客户端等待消息处理时本身的状态(是挂起的还是继续运行的)。

BIO VS NIO

BIO设计原理:服务器通过一个Acceptor线程负责监听客户端请求和为每个客户端创建一个新的线程进行链路处理,典型的一请求一应答模式,若客户端数量增多,频繁地创建和销毁线程会给服务器带来很大的压力,后改良为用线程池的方式代替新增线程,被称为伪异步IO。服务器端使用线程池来处理客户端发起的IO请求,虽然,可以避免产生大量的线程导致JVM内存溢出,但是,当一个线程调用 read() 或 write() 方法时会发生阻塞,这样,当新的连接请求过来时,会导致线程池中的阻塞队列满,客户端也会出现连接超时现象。

NIO设计原理:客户端和服务器之间通过Channel通信,NIO可以在Channel进行读写操作,这些Channel都会被注册在Selector多路复用器上,Selector通过一个线程不停的轮询这些Channel,找出已经准备就绪的Channel执行IO操作。

Scoket网络编程

  • TCP(Transport Control Protocol)是一个可靠(可靠通信的含义是指有确认)的、面向连接的协议。
  • UDP(User Datagram Protocol)是一个不可靠的、面向无连接的协议。
  • 面向连接的服务是按照电话系统建模的,面向无连接的服务是按照邮政系统建模的。
  • Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口;在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

Socket编程

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
@Slf4j
public class TCPServer {

private final int port;
private final ServerSocket serverSocket;

public TCPServer(int port) throws IOException {
this.port = port;
this.serverSocket = new ServerSocket(port);
}

public void listen() throws IOException {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
log.debug("Server started listening on port {}", port);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞
// new WorkerThread(socket).start();
threadPool.execute(new WorkerThread(socket));
}
}

static class WorkerThread extends Thread {

private final Socket socket;

public WorkerThread(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
try {
// 读取客户端数据
DataInputStream in = new DataInputStream(socket.getInputStream());
// 向客户端发送数据
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
while (!socket.isClosed()) {
String line = in.readUTF(); // 阻塞
out.writeUTF(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
closeQuietly(socket);
}
}

private void closeQuietly(final Socket sock) {
if (sock != null) {
try {
sock.close();
} catch (IOException e) {
// ignored
}
}
}
}

public static void main(String[] args) throws IOException {
new TCPServer(8888).listen();
}

}

@Slf4j
public class TCPClient {

private static final String HOST = "127.0.0.1";
private static final int PORT = 8888;

public static void main(String[] args) {
try (Socket socket = new Socket(HOST, PORT)) {
// 读取服务端数据
DataInputStream in = new DataInputStream(socket.getInputStream());
// 向服务端发送数据
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
log.info("Type your message here to send...");
String line;
while ((line = reader.readLine()) != null) {
out.writeUTF(line);
String ret = in.readUTF();
log.debug("Receive messages from the server: {}", ret);
}
} catch (IOException e) {
e.printStackTrace();
}
}

}

@Slf4j
public class UDPSender {

private static final String HOST = "127.0.0.1";
private static final int PORT = 8888;

public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket();
InetAddress inetAddress = InetAddress.getByName(HOST);
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
log.info("Type your message here to send");
String line;
while ((line = in.readLine()) != null) {
byte[] buf = line.getBytes();
socket.send(new DatagramPacket(buf, buf.length, inetAddress, PORT));
}
} catch (IOException e) {
e.printStackTrace();
}
}

}

@Slf4j
public class UDPReceiver {

private static final int PORT = 8888;

public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket(PORT);
while (!socket.isClosed()) {
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
log.info(new String(buf, 0, packet.getLength()));
}
} catch (IOException e) {
e.printStackTrace();
}
}

}