Websockets ได้รับความนิยมมากขึ้นทุกวัน เราได้ยินมาว่าพวกเขาคือ "อนาคต" เราได้ยินมาว่าใช้งานได้ง่ายกว่าที่เคยด้วย ActionCable ใน Rails 5 แต่จริงๆ แล้ว websockets คืออะไรกันแน่? ทำงานอย่างไร
ในโพสต์นี้ เราจะตอบคำถามเหล่านี้โดยการสร้างเซิร์ฟเวอร์ WebSocket แบบง่ายๆ ตั้งแต่เริ่มต้นใน Ruby เมื่อเสร็จแล้ว เราจะสามารถสื่อสารแบบสองทิศทางระหว่างเบราว์เซอร์และเซิร์ฟเวอร์ของเราได้
รหัสในโพสต์นี้มีขึ้นเพื่อเป็นแบบฝึกหัดการเรียนรู้ หากคุณต้องการใช้งาน websockets ในแอปที่ใช้งานจริง ให้ลองดู websocket-ruby gem ที่ยอดเยี่ยม คุณอาจดูที่ WebSocket Spec.
คุณจึงไม่เคยได้ยินเกี่ยวกับ websockets มาก่อน
เว็บซ็อกเก็ตถูกคิดค้นขึ้นเพื่อแก้ปัญหาบางอย่างในการเชื่อมต่อ HTTP ปกติ เมื่อคุณขอเว็บเพจโดยใช้การเชื่อมต่อ HTTP ปกติ เซิร์ฟเวอร์จะส่งเนื้อหาให้คุณแล้วปิดการเชื่อมต่อ หากคุณต้องการขอหน้าอื่น คุณต้องทำการเชื่อมต่อใหม่ ปกติแล้ววิธีนี้ใช้ได้ดี แต่ไม่ใช่วิธีที่ดีที่สุดสำหรับกรณีการใช้งานบางกรณี:
- สำหรับบางแอปพลิเคชัน เช่น แชท จำเป็นต้องอัปเดตส่วนหน้าทันทีที่มีข้อความใหม่เข้ามา หากคุณมีเพียงคำขอ HTTP ปกติ แสดงว่าคุณต้องสำรวจเซิร์ฟเวอร์อย่างต่อเนื่องเพื่อดูว่ามีหรือไม่ เนื้อหาใหม่
- หากแอปพลิเคชันส่วนหน้าของคุณต้องการส่งคำขอจำนวนเล็กน้อยไปยังเซิร์ฟเวอร์ ค่าใช้จ่ายในการสร้างการเชื่อมต่อใหม่สำหรับแต่ละคำขออาจกลายเป็นปัญหาด้านประสิทธิภาพ นี่เป็นปัญหาน้อยกว่าใน HTTP2
เมื่อใช้ซ็อกเก็ตเว็บ คุณจะทำการเชื่อมต่อกับเซิร์ฟเวอร์หนึ่งรายการ จากนั้นเปิดค้างไว้และใช้สำหรับการสื่อสารแบบสองทิศทาง
ฝั่งไคลเอ็นต์
โดยปกติเว็บซ็อกเก็ตจะใช้สำหรับการสื่อสารระหว่างเบราว์เซอร์และเว็บเซิร์ฟเวอร์ ฝั่งเบราว์เซอร์มีการใช้งานใน JavaScript ในตัวอย่างด้านล่าง ฉันได้เขียน JavaScript แบบง่ายๆ เพื่อเปิดซ็อกเก็ตเว็บไปยังเซิร์ฟเวอร์ในเครื่องของฉันและส่งข้อความไปที่มัน
<!doctype html>
<html lang="en">
<head>
<title>Websocket Client</title>
</head>
<body>
<script>
var exampleSocket = new WebSocket("ws://localhost:2345");
exampleSocket.onopen = function (event) {
exampleSocket.send("Can you hear me?");
};
exampleSocket.onmessage = function (event) {
console.log(event.data);
}
</script>
</body>
</html>
หากฉันเปิดเซิร์ฟเวอร์สแตติกขนาดเล็กและเปิดไฟล์นี้ในเว็บเบราว์เซอร์ ฉันจะได้รับข้อผิดพลาด สมเหตุสมผลแล้วเพราะยังไม่มีเซิร์ฟเวอร์ เรายังต้องสร้าง :-)
การเริ่มต้นเซิร์ฟเวอร์
เว็บซ็อกเก็ตเริ่มต้นชีวิตตามคำขอ HTTP ปกติ พวกมันมีวงจรชีวิตที่แปลก:
- เบราว์เซอร์ส่งคำขอ HTTP ปกติ โดยมีส่วนหัวพิเศษที่ระบุว่า "โปรดสร้าง websocket ให้ฉัน"
- เซิร์ฟเวอร์ตอบกลับด้วยการตอบสนอง HTTP บางอย่าง แต่ไม่ได้ปิดการเชื่อมต่อ
- เบราว์เซอร์และเซิร์ฟเวอร์ใช้โปรโตคอล websocket พิเศษเพื่อแลกเปลี่ยนเฟรมข้อมูลผ่านการเชื่อมต่อแบบเปิด
ดังนั้นขั้นตอนแรกสำหรับเราคือการสร้างเว็บเซิร์ฟเวอร์ ในโค้ดด้านล่าง ฉันกำลังสร้างเว็บเซิร์ฟเวอร์ที่ง่ายที่สุด มันไม่ได้ให้บริการอะไรจริงๆ เพียงแค่รอคำขอแล้วพิมพ์ไปที่ STDERR
require 'socket'
server = TCPServer.new('localhost', 2345)
loop do
# Wait for a connection
socket = server.accept
STDERR.puts "Incoming Request"
# Read the HTTP request. We know it's finished when we see a line with nothing but \r\n
http_request = ""
while (line = socket.gets) && (line != "\r\n")
http_request += line
end
STDERR.puts http_request
socket.close
end
ถ้าฉันเปิดเซิร์ฟเวอร์ และรีเฟรชหน้าทดสอบ websocket ฉันจะได้รับสิ่งนี้:
$ ruby server1.rb
Incoming Request
GET / HTTP/1.1
Host: localhost:2345
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: cG8zEwcrcLnEftn2qohdKQ==
หากคุณจะสังเกตเห็น คำขอ HTTP นี้มีส่วนหัวจำนวนมากที่เกี่ยวข้องกับซ็อกเก็ตเว็บ นี่เป็นขั้นตอนแรกในการจับมือกันของ websocket
การจับมือกัน
คำขอเว็บซ็อกเก็ตทั้งหมดเริ่มต้นด้วยการจับมือกัน ทั้งนี้เพื่อให้แน่ใจว่าทั้งไคลเอ็นต์และเซิร์ฟเวอร์ต่างเข้าใจว่าซ็อกเก็ตเว็บกำลังจะเกิดขึ้น และทั้งคู่ก็เห็นด้วยกับเวอร์ชันโปรโตคอล มันทำงานดังนี้:
ไคลเอนต์ส่งคำขอ HTTP แบบนี้
GET / HTTP/1.1
Host: localhost:2345
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: E4i4gDQc1XTIQcQxvf+ODA==
Sec-WebSocket-Version: 13
ส่วนที่สำคัญที่สุดของคำขอนี้คือ Sec-WebSocket-Key
. ไคลเอ็นต์คาดหวังให้เซิร์ฟเวอร์ส่งคืนเวอร์ชันที่แก้ไขของค่านี้เพื่อเป็นหลักฐานในการโจมตี XSS และแคชพร็อกซี
เซิร์ฟเวอร์ตอบสนอง
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: d9WHst60HtB4IvjOVevrexl0oLA=
การตอบสนองของเซิร์ฟเวอร์เป็นแบบสำเร็จรูป ยกเว้น Sec-WebSocket-Accept
หัวข้อ. ส่วนหัวนี้ถูกสร้างขึ้นดังนี้:
# Take the value provided by the client, append a magic
# string to it. Generate the SHA1 hash, then base64 encode it.
Digest::SHA1.base64digest([sec_websocket_accept, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
ดวงตาของคุณไม่ได้โกหกคุณ มีค่าคงที่เวทย์มนตร์ที่เกี่ยวข้อง
การจับมือกัน
มาอัปเดตเซิร์ฟเวอร์ของเราเพื่อจับมือกันให้เสร็จ ก่อนอื่น เราจะดึงโทเค็นความปลอดภัยออกจากส่วนหัวของคำขอ:
# Grab the security key from the headers.
# If one isn't present, close the connection.
if matches = http_request.match(/^Sec-WebSocket-Key: (\S+)/)
websocket_key = matches[1]
STDERR.puts "Websocket handshake detected with key: #{ websocket_key }"
else
STDERR.puts "Aborting non-websocket connection"
socket.close
next
end
ตอนนี้ เราใช้คีย์ความปลอดภัยเพื่อสร้างการตอบกลับที่ถูกต้อง:
response_key = Digest::SHA1.base64digest([websocket_key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
STDERR.puts "Responding to handshake with key: #{ response_key }"
socket.write <<-eos
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: #{ response_key }
eos
STDERR.puts "Handshake completed."
เมื่อฉันรีเฟรชหน้าทดสอบ websocket ฉันพบว่าไม่มีข้อผิดพลาดในการเชื่อมต่ออีกต่อไป การเชื่อมต่อถูกสร้างขึ้น!
นี่คือผลลัพธ์จากเซิร์ฟเวอร์ที่แสดงคีย์ความปลอดภัยและคีย์ตอบกลับ:
$ ruby server2.rb
Incoming Request
Websocket handshake detected with key: Fh06+WnoTQQiVnX5saeYMg==
Responding to handshake with key: nJg1c2upAHixOmXz7kV2bJ2g/YQ=
Handshake completed.
โปรโตคอลเฟรมเว็บซ็อกเก็ต
เมื่อสร้างการเชื่อมต่อ WebSocket แล้ว HTTP จะไม่ถูกใช้อีกต่อไป ข้อมูลจะถูกแลกเปลี่ยนผ่านโปรโตคอล WebSocket แทน
เฟรมเป็นหน่วยพื้นฐานของโปรโตคอล WebSocket
โปรโตคอล WebSocket เป็นแบบอิงเฟรม แต่สิ่งนี้หมายความว่าอย่างไร
เมื่อใดก็ตามที่คุณขอให้เว็บเบราว์เซอร์ส่งข้อมูลผ่าน WebSocket หรือขอให้เซิร์ฟเวอร์ตอบสนอง ข้อมูลจะถูกแบ่งออกเป็นชุดๆ ในแต่ละส่วนจะถูกห่อด้วยข้อมูลเมตาเพื่อสร้างเฟรม
นี่คือลักษณะโครงสร้างเฟรม ตัวเลขด้านบนเป็นบิต และฟิลด์บางฟิลด์ เช่น ความยาวเพย์โหลดเพิ่มเติม อาจไม่ปรากฏตลอดเวลา:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
สิ่งแรกที่อาจกระโดดออกมาที่คุณคือนี่คือโปรโตคอลไบนารี เราจะต้องจัดการเล็กน้อย แต่ไม่ต้องกังวล มันจะไม่ยากขนาดนั้น ตัวเลขด้านบนเป็นบิต และบางสาขาก็อาจไม่ปรากฏให้เห็นเสมอไป ตัวอย่างเช่น ความยาวเพย์โหลดที่ขยายจะปรากฏหากเพย์โหลดต่ำกว่า 127 ไบต์
กำลังรับข้อมูล
ตอนนี้การจับมือของพวกเขาเสร็จสิ้นแล้ว เราสามารถเริ่มแยกวิเคราะห์เฟรมไบนารีได้ เพื่อให้ง่ายขึ้น เราจะดูที่เฟรมที่เข้ามาทีละไบต์ หลังจากนั้น เราจะนำทุกอย่างมารวมกันเพื่อให้คุณได้ใช้งานจริง
ไบต์ 1:FIN และ Opcode
จากตารางด้านบน คุณจะเห็นว่าไบต์แรก (แปดบิตแรก) มีข้อมูลสองสามส่วน:
- FIN:1 บิต หากเป็นเท็จ ข้อความจะถูกแบ่งออกเป็นหลายเฟรม
- opcode:4 บิต บอกเราว่าเพย์โหลดเป็นข้อความ ไบนารี หรือนี่เป็นเพียง "ปิง" เพื่อให้การเชื่อมต่อคงอยู่
- RSV:3 บิต สิ่งเหล่านี้ไม่ได้ใช้ในข้อมูลจำเพาะ WebSockets ปัจจุบัน
ในการรับไบต์แรก เราจะใช้ IO#getbyte
กระบวนการ. และในการดึงข้อมูล เราจะใช้บิตมาสก์ง่ายๆ หากคุณไม่คุ้นเคยกับตัวดำเนินการระดับบิต ให้อ่านบทความอื่นของฉัน Bitwise hacks in Ruby
first_byte = socket.getbyte
fin = first_byte & 0b10000000
opcode = first_byte & 0b00001111
# Our server will only support single-frame, text messages.
# Raise an exception if the client tries to send anything else.
raise "We don't support continuations" unless fin
raise "We only support opcode 1" unless opcode == 1
ไบต์ 2:MASK และความยาวของเพย์โหลด
ไบต์ที่สองของเฟรมมีข้อมูลเพิ่มเติมเกี่ยวกับเพย์โหลด
- มาสก์:1 บิต แฟล็กบูลีนระบุว่าเพย์โหลดถูกปิดบังหรือไม่ หากเป็นจริง จะต้อง "เปิดโปง" เพย์โหลดก่อนใช้งาน สิ่งนี้ควรเป็นจริงเสมอสำหรับเฟรมที่มาจากลูกค้าของเรา สเปกบอกไว้
- ความยาวของเพย์โหลด:7 บิต หากเพย์โหลดของเราน้อยกว่า 126 ไบต์ ความยาวจะถูกเก็บไว้ที่นี่ หากค่านี้มากกว่า 126 แสดงว่าจะมีจำนวนไบต์ตามมามากขึ้นเพื่อให้เรามีความยาว
นี่คือวิธีที่เราจัดการกับไบต์ที่สอง:
second_byte = socket.getbyte
is_masked = second_byte & 0b10000000
payload_size = second_byte & 0b01111111
raise "All frames sent to a server should be masked according to the websocket spec" unless is_masked
raise "We only support payloads < 126 bytes in length" unless payload_size < 126
STDERR.puts "Payload size: #{ payload_size } bytes"
ไบต์ 3-7:คีย์การกำบัง
เราคาดว่าเพย์โหลดของเฟรมที่เข้ามาทั้งหมดจะถูกปิดบัง ในการเปิดโปงเนื้อหา เราจะต้อง XOR กับคีย์การกำบัง
คีย์กำบังนี้ประกอบขึ้นเป็นสี่ไบต์ถัดไป เราไม่ต้องประมวลผลเลย เราแค่อ่านไบต์ลงในอาร์เรย์
mask = 4.times.map { socket.getbyte }
STDERR.puts "Got mask: #{ mask.inspect }"
โปรดบอกฉันถ้าคุณรู้วิธีที่ดีกว่าในการอ่าน 4 ไบต์ในอาร์เรย์
times.map
ค่อนข้างแปลก แต่ก็เป็นวิธีที่รัดกุมที่สุดที่ฉันคิดได้ ฉันชื่อ @StarrHorne บนทวิตเตอร์
ไบต์ 8 ขึ้นไป:เพย์โหลด
เอาล่ะ เราเสร็จสิ้นข้อมูลเมตาแล้ว ตอนนี้สามารถดึงข้อมูลเพย์โหลดที่แท้จริงได้
data = payload_size.times.map { socket.getbyte }
STDERR.puts "Got masked data: #{ data.inspect }"
โปรดจำไว้ว่าเพย์โหลดนี้ถูกปิดบัง ดังนั้นถ้าพิมพ์ออกมาก็จะดูเหมือนขยะ ในการเปิดโปง เราเพียงแค่ XOR แต่ละไบต์ด้วยไบต์ที่สอดคล้องกันของมาสก์ เนื่องจากมาสก์มีความยาวเพียง 4 ไบต์ เราจึงทำซ้ำเพื่อให้ตรงกับความยาวของเพย์โหลด:
unmasked_data = data.each_with_index.map { |byte, i| byte ^ mask[i % 4] }
STDERR.puts "Unmasked the data: #{ unmasked_data.inspect }"
ตอนนี้เรามีอาร์เรย์ของไบต์ เราต้องแปลงเป็นสตริงยูนิโค้ด ข้อความทั้งหมดใน Websockets เป็นยูนิโค้ด
STDERR.puts "Converted to a string: #{ unmasked_data.pack('C*').force_encoding('utf-8').inspect }"
รวมทุกอย่างเข้าด้วยกัน
เมื่อคุณรวมรหัสทั้งหมดนี้เข้าด้วยกัน คุณจะได้สคริปต์ที่มีลักษณะดังนี้:
require 'socket' # Provides TCPServer and TCPSocket classes
require 'digest/sha1'
server = TCPServer.new('localhost', 2345)
loop do
# Wait for a connection
socket = server.accept
STDERR.puts "Incoming Request"
# Read the HTTP request. We know it's finished when we see a line with nothing but \r\n
http_request = ""
while (line = socket.gets) && (line != "\r\n")
http_request += line
end
# Grab the security key from the headers. If one isn't present, close the connection.
if matches = http_request.match(/^Sec-WebSocket-Key: (\S+)/)
websocket_key = matches[1]
STDERR.puts "Websocket handshake detected with key: #{ websocket_key }"
else
STDERR.puts "Aborting non-websocket connection"
socket.close
next
end
response_key = Digest::SHA1.base64digest([websocket_key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
STDERR.puts "Responding to handshake with key: #{ response_key }"
socket.write <<-eos
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: #{ response_key }
eos
STDERR.puts "Handshake completed. Starting to parse the websocket frame."
first_byte = socket.getbyte
fin = first_byte & 0b10000000
opcode = first_byte & 0b00001111
raise "We don't support continuations" unless fin
raise "We only support opcode 1" unless opcode == 1
second_byte = socket.getbyte
is_masked = second_byte & 0b10000000
payload_size = second_byte & 0b01111111
raise "All incoming frames should be masked according to the websocket spec" unless is_masked
raise "We only support payloads < 126 bytes in length" unless payload_size < 126
STDERR.puts "Payload size: #{ payload_size } bytes"
mask = 4.times.map { socket.getbyte }
STDERR.puts "Got mask: #{ mask.inspect }"
data = payload_size.times.map { socket.getbyte }
STDERR.puts "Got masked data: #{ data.inspect }"
unmasked_data = data.each_with_index.map { |byte, i| byte ^ mask[i % 4] }
STDERR.puts "Unmasked the data: #{ unmasked_data.inspect }"
STDERR.puts "Converted to a string: #{ unmasked_data.pack('C*').force_encoding('utf-8').inspect }"
socket.close
end
เมื่อฉันรีเฟรชหน้าเว็บผู้ทดสอบ WebSocket และส่งคำขอไปยังเซิร์ฟเวอร์ของฉัน นี่คือผลลัพธ์ที่ฉันเห็น:
$ ruby websocket_server.rb
Incoming Request
Websocket handshake detected with key: E4i4gDQc1XTIQcQxvf+ODA==
Responding to handshake with key: d9WHst60HtB4IvjOVevrexl0oLA=
Handshake completed. Starting to parse the websocket frame.
Payload size: 16 bytes
Got mask: [80, 191, 161, 254]
Got masked data: [19, 222, 207, 222, 41, 208, 212, 222, 56, 218, 192, 140, 112, 210, 196, 193]
Unmasked the data: [67, 97, 110, 32, 121, 111, 117, 32, 104, 101, 97, 114, 32, 109, 101, 63]
Converted to a string: "Can you hear me?"
การส่งข้อมูลกลับไปยังไคลเอนต์
ดังนั้นเราจึงส่งข้อความทดสอบจากลูกค้าของเราไปยังเซิร์ฟเวอร์ของเล่น WebSocket ของเราได้สำเร็จ ตอนนี้คงเป็นการดีที่จะส่งข้อความกลับจากเซิร์ฟเวอร์ไปยังไคลเอนต์
สิ่งนี้เกี่ยวข้องน้อยกว่าเล็กน้อย เนื่องจากเราไม่ต้องจัดการกับสิ่งปิดบังใดๆ เฟรมที่ส่งจากเซิร์ฟเวอร์ไปยังไคลเอนต์จะถูกเปิดออกเสมอ
เช่นเดียวกับที่เราใช้เฟรมครั้งละหนึ่งไบต์ เราจะสร้างมันครั้งละหนึ่งไบต์
ไบต์ 1:FIN และ opcode
เพย์โหลดของเราจะพอดีกับเฟรมเดียว และมันจะเป็นข้อความ นั่นหมายความว่า FIN จะเท่ากับ 1 และ opcode ก็เท่ากับหนึ่งด้วย เมื่อฉันรวมกลุ่มที่ใช้รูปแบบบิตเดียวกันกับที่เราเคยใช้มาก่อน ฉันได้ตัวเลข:
output = [0b10000001]
ไบต์ 2:MASKED และความยาวของเพย์โหลด
เนื่องจากเฟรมนี้เปลี่ยนจากเซิร์ฟเวอร์ไปยังไคลเอ็นต์ MASKED จะเท่ากับศูนย์ นั่นหมายความว่าเราสามารถเพิกเฉยได้ ความยาวเพย์โหลดเป็นเพียงความยาวของสตริง
output = [0b10000001, response.size]
ไบต์ 3 ขึ้นไป:เพย์โหลด
เพย์โหลดไม่ได้ถูกปิดบัง มันเป็นแค่สตริง
response = "Loud and clear!"
STDERR.puts "Sending response: #{ response.inspect }"
output = [0b10000001, response.size, response]
ระเบิดทิ้ง!
ณ จุดนี้ เรามีอาร์เรย์ที่มีข้อมูลที่เราต้องการส่ง เราจำเป็นต้องแปลงค่านี้เป็นสตริงไบต์ที่เราสามารถส่งผ่านสายได้ ในการทำเช่นนี้ เราจะใช้ Array#pack
. อเนกประสงค์ วิธีการ
socket.write output.pack("CCA#{ response.size }")
สตริงแปลก ๆ "CCA#{ response.size }"
บอก Array#pack
ที่อาร์เรย์มีอินต์ 8 บิตที่ไม่ได้ลงชื่อสองตัว ตามด้วยสตริงอักขระที่มีขนาดที่ระบุ
หากฉันเปิดโปรแกรมตรวจสอบเครือข่ายใน Chrome ฉันจะเห็นว่าข้อความส่งเสียงดังและชัดเจน
เครดิตเสริม
แค่นั้นแหละ! ฉันหวังว่าคุณจะได้เรียนรู้บางอย่างเกี่ยวกับ WebSockets มีหลายสิ่งหลายอย่างที่เซิร์ฟเวอร์ขาดหายไป หากคุณต้องการออกกำลังกายต่อไป คุณอาจพิจารณา:
- รองรับเพย์โหลดแบบหลายเฟรม
- รองรับเพย์โหลดไบนารี
- รองรับปิง / ปิงปอง
- รองรับน้ำหนักบรรทุกนาน
- ปิดการจับมือ