深入Socket-TCP
什么是TCP
TCP-Transmission Control Protocal-即传输控制协议,是一种面向连接的,可靠的,基于字节流的传输层通信协议。(UDP是无连接,不可靠,基于数据报文的传输层通信协议)
TCP的机制
连接可靠性-三次握手
所谓三次握手,是指建立一个TCP连接时,需要客户端和服务器总共发送三个包
- 第一次握手(SYN=1,seq=x)
客户端发送一个TCP的SYN标志位置为1的包,指明客户端打算连接的服务器的端口,以及初始化序号x(用于在多客户端申请的场景下标识客户端,是随机生成的,叫做ISN序号)保存在包头的序列化(Sequence Number)字段里。发送完毕,客户端进入SYN_SEND状态 - 第二次握手(SYN=1,ACK=1,seq=y,ACKnum=x+1)
服务器发回确认包(ACK)应答,即SYN标志位和ACK标志位均为1.服务器选择自己的ISN***放到Seq域内同时将确认序号(Acknowledgement Number)设置为客户的ISN+1,即x+1。发送完毕后 ,服务器端进入SYN_RCVD状态 - 第三次握手(ACK=1,ACKnum=y+1)
客户端再次发送确认包(ACK),SYN标志位为0,并且将确认序号(ACKnum)放服务器端ISN的+1.发送完毕后,客户端进入ESTABLTISHED状态,当服务器接收到这个包时,也进入ESTABLTISHED,代表TCP握手结束。
连接可靠性-四次挥手
TCP连接的拆除需要发送四个包,因此称为四次挥手。客户端和服务器均可发起挥手的行为。
-
第一次挥手(FIN=1,seq=x)
假设客户端想要关闭连接,客户端发送一个FIN标志位置为1的包,表示自己没有数据可以发送了,但是仍然可以接收数据。发送完毕后,客户端进入FIN_WAIT_1状态 -
第二次挥手(ACK=1,ACKnum=x+1)
服务器确认客户端的FIN包,发送一个确认包,表明自己接收到了来自客户端的关闭连接的请求,但还没有准备好关闭连接,此时可能仍然有未传输完的数据。发送完毕后,服务器端进入CLOSE_WAIT状态。客户端接收到这个确认包之后,进入FIN_WAIT_2状态,等待服务器关闭连接 -
第三次挥手(FIN=1,seq=y)
服务器准备好关闭连接时,向客户端发送结束连接请求,FIN置为1.发送完毕后,服务器进入LAST_ACK状态,等待来自客户端的最后一个ACK -
第四次挥手(ACK=1,ACKnum=y+1)
客户端接收到来自服务器的关闭请求,发送一个确认包,并进入TIME_WAIT状态,等待可能要出现的要求重传的ACK包。服务器接收到这个确认包后,关闭连接,进入CLOSED状态。
客户端等待某个固定时间(两个最大段生命周期,2MSL–2 Maximum Segment Lifetime)之后,没有收到服务器端的ACK,认为服务器端已经正常关闭,于是自己也关闭连接,进入CLOSED状态
关于握手挥手中的字段
- SYN-创建一个连接
- FIN-终结一个链接
- ACK-确认接收到数据
- Sequence Number-当前连接的初始***
- ACKnum-指接收方期待的下一个报文段的***
不管客户端是想要建立连接还是断开连接,要么就发送请求时将SYN标志位置1,要么就将FIN标志位置1,并发送当前的***,即Seq。服务器端接收到来自客户端的请求后,会先将ACK置1,表示已经接收到了来自客户端的包,并将来自客户端的***+1赋给ACKnum同时返回给客户端,之后就根据是握手请求还是挥手请求进行不同的操作:
- 握手请求:除了ACK置1确认响应以及ACKnum=x+1外,服务器重复客户端先前的行为,期待来自客户端的响应。这就好比打电话时:A问"你听到我说话了吗(询问)",B说"我听到了(响应),那你听到我说话了吗(询问)",A又回答"听到了(响应)",之后A和B就可以说正事了。
- 挥手请求:客户端申请挥手请求后,服务器端第一次响应了ACK=1和ACKnum=x+1外没有响应其他的,等待剩余的数据发送完,服务器才继续发送FIN=1,Seq=y去和客户端挥手。这就好比,A和B说拜拜,B没有说其他的,只是先说我知道了,之后B把手上的东西全部交给A后,B才继续说“那你可以走了”,之后A响应B就关闭连接
SYN攻击
什么是SYN攻击?
在三次握手过程中,服务器发送 SYN-ACK 之后,收到客户端的 ACK 之前的 TCP 连接称为半连接(half-open connect)。此时服务器处于 SYN_RCVD 状态。当收到 ACK 后,服务器才能转入 ESTABLISHED 状态
SYN攻击指的是,攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN包,服务器回复确认包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪,SYN 攻击是一种典型的 DoS/DDoS 攻击。
如何防御 SYN 攻击?
- 缩短超时(SYN Timeout)时间
- 增加最大半连接数
- 过滤网关防护
- SYN cookies技术
TCP KeepAlive
TCP 的连接,实际上是一种纯软件层面的概念,在物理层面并没有“连接”这种概念。TCP 通信双方建立交互的连接,但是并不是一直存在数据交互,有些连接会在数据交互完毕后,主动释放连接,而有些不会。在长时间无数据交互的时间段内,交互双方都有可能出现掉电、死机、异常重启等各种意外,当这些意外发生之后,这些 TCP 连接并未来得及正常释放,在软件层面上,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,为了解决这个问题,在传输层可以利用 TCP 的 KeepAlive 机制实现来实现。主流的操作系统基本都在内核里支持了这个特性。
TCP KeepAlive 的基本原理是,隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。
传输可靠性
当用TCP进行一条数据发送的时候,首先TCP会将这条数据拆分成不同的片段,然后把片段进行排序,把排序好的片段顺序的进行组装进行发送
- 应用数据被分割成TCP认为最合适发送的块。这点和UDP不同,应用程序产生的数据报长度将保持不变。由TCP传递给IP的信息单位称为报文段或者段(segment)
- 当TCP发出一个段之后,它会启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段
- 当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒
- TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检验数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段,并且不确认收到此报文段,从而希望发送端超时并重发
- 既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层
- 既然IP数据报会发生重复,TCP的接收端必须丢弃重复的数据
- TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机缓冲区溢出
Sokcet中的IO流以及套接字配置
当客户端的socket连接到服务器的套接字时,类似:
socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 3000);
当套接字建立后,类似一条通信线路已经建立。socket可以使用getInputStream()方法获得一个输入流,用此输入流读取服务器放入"线路"中的信息(注意不能读取自己放入线路中的信息,就像打电话的听筒一样),与此对应的socket还可以用getOutputStream方法获得一个输出流,即拿到了自己的话筒,将自己的信息放入"线路"中。
ServerSocket类
客户端建立socket连接只是负责呼叫服务器,那么服务器也要建立一个等待接收客户的套接字serversocket对象
//与上面的端口号一致
ServerSocket server=new ServerSocket(30000);
Socket sc=server_socket.accept();
服务器的ServerSocket对象server建立后,可以使用accept()方法接收客户的套接字响应。
所谓接收"客户"的套接字连接就是accept()方法会返回一个和客户端Scoket对象相连接的Socket对象(注意:server调用accept方法返回的不是客户端Socket对象,而是和客户端Socket对象相连接的Socket对象,听起来有点绕,但是只有真正理解这个才能理解IO流的流向),服务端的Socket对象sc使用getOutputStream()方法获得的输出流将指向客户端Socket对象使用getInputStream()方法获得的输入流,这就像打电话时那样接通彼此,同样sc使用getInputStream()方法获得的输入流将指向客户端socket对象的输出流
异步处理客户Socket
使用套接字连接时,可能在一端没发出数据之前,另一端就开始连接,这样就会堵塞线程,或者accept()方法在未获得客户Socket时,也会一直堵塞直到读取。通过将套接字放到线程中可以实现异步化处理,解决同步下阻塞的问题
// 等待客户端连接
for (; ; ) {
// 得到客户端
Socket client = server.accept();
// 客户端构建异步线程
ClientHandler clientHandler = new ClientHandler(client);
// 启动线程
clientHandler.start();
}
private static class ClientHandler extends Thread {
//...
}
Demo
客户端
public class Client {
private static final int PORT = 20000;
private static final int LOCAL_PORT = 20001;
public static void main(String[] args) throws IOException {
Socket socket = createSocket();
initSocket(socket);
// 链接到本地20000端口,超时时间3秒,超过则抛出超时异常
socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 3000);
System.out.println("已发起服务器连接,并进入后续流程~");
System.out.println("客户端信息:" + socket.getLocalAddress() + " P:" + socket.getLocalPort());
System.out.println("服务器信息:" + socket.getInetAddress() + " P:" + socket.getPort());
try {
// 发送接收数据
todo(socket);
} catch (Exception e) {
System.out.println("异常关闭");
}
// 释放资源
socket.close();
System.out.println("客户端已退出~");
}
private static Socket createSocket() throws IOException {
/*
// 无代理模式,等效于空构造函数
Socket socket = new Socket(Proxy.NO_PROXY);
// 新建一份具有HTTP代理的套接字,传输数据将通过www.baidu.com:8080端口转发
Proxy proxy = new Proxy(Proxy.Type.HTTP,
new InetSocketAddress(Inet4Address.getByName("www.baidu.com"), 8800));
socket = new Socket(proxy);
// 新建一个套接字,并且直接链接到本地20000的服务器上
socket = new Socket("localhost", PORT);
// 新建一个套接字,并且直接链接到本地20000的服务器上
socket = new Socket(Inet4Address.getLocalHost(), PORT);
// 新建一个套接字,并且直接链接到本地20000的服务器上,并且绑定到本地20001端口上
socket = new Socket("localhost", PORT, Inet4Address.getLocalHost(), LOCAL_PORT);
socket = new Socket(Inet4Address.getLocalHost(), PORT, Inet4Address.getLocalHost(), LOCAL_PORT);
*/
Socket socket = new Socket();
// 绑定到本地20001端口
socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), LOCAL_PORT));
return socket;
}
private static void initSocket(Socket socket) throws SocketException {
// 设置读取超时时间为2秒
socket.setSoTimeout(2000);
// 是否复用未完全关闭的Socket地址,对于指定bind操作后的套接字有效
socket.setReuseAddress(true);
// 是否开启Nagle算法
socket.setTcpNoDelay(true);
// 是否需要在长时无数据响应时发送确认数据(类似心跳包),时间大约为2小时
socket.setKeepAlive(true);
// 对于close关闭操作行为进行怎样的处理;默认为false,0
// false、0:默认情况,关闭时立即返回,底层系统接管输出流,将缓冲区内的数据发送完成
// true、0:关闭时立即返回,缓冲区数据抛弃,直接发送RST结束命令到对方,并无需经过2MSL等待
// true、200:关闭时最长阻塞200毫秒,随后按第二情况处理
socket.setSoLinger(true, 20);
// 是否让紧急数据内敛,默认false;紧急数据通过 socket.sendUrgentData(1);发送
socket.setOOBInline(true);
// 设置接收发送缓冲器大小
socket.setReceiveBufferSize(64 * 1024 * 1024);
socket.setSendBufferSize(64 * 1024 * 1024);
// 设置性能参数:短链接,延迟,带宽的相对重要性
socket.setPerformancePreferences(1, 1, 0);
}
private static void todo(Socket client) throws IOException {
// 得到Socket输出流
OutputStream outputStream = client.getOutputStream();
// 得到Socket输入流
InputStream inputStream = client.getInputStream();
byte[] buffer = new byte[256];
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
// byte
byteBuffer.put((byte) 126);
// char
char c = 'a';
byteBuffer.putChar(c);
// int
int i = 2323123;
byteBuffer.putInt(i);
// bool
boolean b = true;
byteBuffer.put(b ? (byte) 1 : (byte) 0);
// Long
long l = 298789739;
byteBuffer.putLong(l);
// float
float f = 12.345f;
byteBuffer.putFloat(f);
// double
double d = 13.31241248782973;
byteBuffer.putDouble(d);
// String
String str = "Hello你好!";
byteBuffer.put(str.getBytes());
// 发送到服务器
outputStream.write(buffer, 0, byteBuffer.position() + 1);
// 接收服务器返回
int read = inputStream.read(buffer);
System.out.println("收到数量:" + read);
// 资源释放
outputStream.close();
inputStream.close();
}
}
服务器端
public class Server {
private static final int PORT = 20000;
public static void main(String[] args) throws IOException {
ServerSocket server = createServerSocket();
initServerSocket(server);
// 绑定到本地端口上
server.bind(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 50);
System.out.println("服务器准备就绪~");
System.out.println("服务器信息:" + server.getInetAddress() + " P:" + server.getLocalPort());
// 等待客户端连接
for (; ; ) {
// 得到客户端
Socket client = server.accept();
// 客户端构建异步线程
ClientHandler clientHandler = new ClientHandler(client);
// 启动线程
clientHandler.start();
}
}
private static ServerSocket createServerSocket() throws IOException {
// 创建基础的ServerSocket
ServerSocket serverSocket = new ServerSocket();
// 绑定到本地端口20000上,并且设置当前可允许等待链接的队列为50个
//serverSocket = new ServerSocket(PORT);
// 等效于上面的方案,队列设置为50个
//serverSocket = new ServerSocket(PORT, 50);
// 与上面等同
// serverSocket = new ServerSocket(PORT, 50, Inet4Address.getLocalHost());
return serverSocket;
}
private static void initServerSocket(ServerSocket serverSocket) throws IOException {
// 是否复用未完全关闭的地址端口
serverSocket.setReuseAddress(true);
// 等效Socket#setReceiveBufferSize
serverSocket.setReceiveBufferSize(64 * 1024 * 1024);
// 设置serverSocket#accept超时时间
// serverSocket.setSoTimeout(2000);
// 设置性能参数:短链接,延迟,带宽的相对重要性
//此处的设置等效客户端的设置
serverSocket.setPerformancePreferences(1, 1, 1);
}
/**
* 客户端消息处理
*/
private static class ClientHandler extends Thread {
private Socket socket;
ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
super.run();
System.out.println("新客户端连接:" + socket.getInetAddress() +
" P:" + socket.getPort());
try {
// 得到套接字流
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[256];
int readCount = inputStream.read(buffer);
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, readCount);
// byte
byte be = byteBuffer.get();
// char
char c = byteBuffer.getChar();
// int
int i = byteBuffer.getInt();
// bool
boolean b = byteBuffer.get() == 1;
// Long
long l = byteBuffer.getLong();
// float
float f = byteBuffer.getFloat();
// double
double d = byteBuffer.getDouble();
// String
int pos = byteBuffer.position();
String str = new String(buffer, pos, readCount - pos - 1);
System.out.println("收到数量:" + readCount + " 数据:"
+ be + "\n"
+ c + "\n"
+ i + "\n"
+ b + "\n"
+ l + "\n"
+ f + "\n"
+ d + "\n"
+ str + "\n");
outputStream.write(buffer, 0, readCount);
outputStream.close();
inputStream.close();
} catch (Exception e) {
System.out.println("连接异常断开");
} finally {
// 连接关闭
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("客户端已退出:" + socket.getInetAddress() +
" P:" + socket.getPort());
}
}
}