การสร้างเซิร์ฟเวอร์ wss

ถาม:

Workerman สร้างเซิร์ฟเวอร์ wss อย่างไร เพื่อให้ไคลเอนต์สามารถเชื่อมต่อสื่อสารผ่าน wss เช่น ในแอพพลิเคชันมินิโปรแกรมของ WeChat。

ตอบ:

โปรโตคอล wss คือ websocket + SSL ซึ่งคือการเพิ่มชั้น SSL ลงในโปรโตคอล websocket คล้ายกับ https (http + SSL)。
ดังนั้นเพียงแค่เปิดใช้งาน SSL บนพื้นฐานของโปรโตคอล websocket ก็จะสามารถรองรับโปรโตคอล wss ได้。

วิธีที่หนึ่ง ใช้ nginx/apache เป็นพร็อกซี่ SSL (แนะนำ)

เหตุผลที่แนะนำ

  • สามารถใช้พอร์ต 443 ได้อีก สามารถเชื่อมต่อได้โดยไม่ต้องระบุพอร์ต
  • การจัดการ SSL certificate อย่างรวมศูนย์ สามารถใช้การกำหนดค่าของเว็บไซต์ได้
  • สามารถทำการโหลดบาลานซ์
  • มีการตรวจสอบล็อกข้อมูลง่าย
  • รองรับความเข้ากันได้ที่ดีขึ้น

หลักการและกระบวนการสื่อสาร

  1. ไคลเอนต์ทำการเชื่อมต่อ wss ไปที่ nginx/apache

  2. nginx/apache จะแปลงข้อมูลโปรโตคอล wss เป็นข้อมูลโปรโตคอล ws และส่งต่อไปยังพอร์ตโปรโตคอล websocket ของ Workerman

  3. Workerman ได้รับข้อมูลแล้วดำเนินการตามตรรกะธุรกิจ

  4. เมื่อ Workerman ส่งข้อความกลับไปให้ไคลเอนต์ จะเป็นกระบวนการตรงกันข้าม ข้อมูลจะถูกแปลงจาก nginx/apache เป็นโปรโตคอล wss แล้วส่งให้ไคลเอนต์

การกำหนดค่า nginx อ้างอิง

เงื่อนไขเบื้องต้นและการเตรียมการ:

  1. ติดตั้ง nginx แล้ว เวอร์ชันไม่ต่ำกว่า 1.3

  2. สมมุติว่า Workerman ฟังที่พอร์ต 8282 (โปรโตคอล websocket)

  3. ได้มีการขอใบรับรอง (pem/crt และไฟล์ key) วางไว้ที่ /etc/nginx/conf.d/ssl

  4. ต้องการใช้ nginx เปิดพอร์ต 443 เพื่อให้บริการพร็อกซี่ wss (สามารถเปลี่ยนพอร์ตตามต้องการ)

  5. โดยปกติแล้ว nginx จะทำงานเป็นเซิร์ฟเวอร์เว็บไซต์สำหรับบริการอื่น สำหรับไม่ให้มีผลกระทบต่อการใช้ไซต์ดั้งเดิม ใช้ที่อยู่ domain.com/wss เป็นทางเข้าของพร็อกซี่ wss หมายความว่าที่อยู่เชื่อมต่อของไคลเอนต์คือ wss://domain.com/wss

การกำหนดค่า nginx จะมีลักษณะดังนี้

server {
  listen 443;
  # การกำหนดค่าชื่อโดเมนถูกละเว้น...

  ssl on;
  ssl_certificate /etc/ssl/server.pem;
  ssl_certificate_key /etc/ssl/server.key;
  ssl_session_timeout 5m;
  ssl_session_cache shared:SSL:50m;
  ssl_protocols SSLv3 SSLv2 TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;

  location /wss {
    proxy_pass http://127.0.0.1:8282;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header X-Real-IP $remote_addr;
  }

  # location / {} การกำหนดค่าอื่นๆ ของไซต์...
}

การทดสอบ

// ใบรับรองจะตรวจสอบชื่อโดเมน โปรดเชื่อมต่อด้วยชื่อโดเมน โดยไม่ต้องระบุพอร์ต
ws = new WebSocket("wss://domain.com/wss");

ws.onopen = function() {
    alert("เชื่อมต่อสำเร็จ");
    ws.send('tom');
    alert("ส่งสตริงหนึ่งไปยังเซิร์ฟเวอร์: tom");
};
ws.onmessage = function(e) {
    alert("ได้รับข้อความจากเซิร์ฟเวอร์: " + e.data);
};

ใช้ apache เป็นพร็อกซี่ wss

สามารถใช้ apache เป็นพร็อกซี่การส่งต่อให้กับ workerman ได้

การเตรียมการ:

  1. GatewayWorker ฟังที่พอร์ต 8282 (โปรโตคอล websocket)

  2. ได้ขอใบรับรอง ssl แล้ว สมมุติว่าอยู่ที่ /server/httpd/cert/

  3. ใช้ apache ส่งต่อพอร์ต 443 ไปที่พอร์ตที่ระบุ 8282

  4. httpd-ssl.conf ถูกโหลดแล้ว

  5. openssl ได้ติดตั้งแล้ว

เปิดใช้งานโมดูล proxy_wstunnel_module

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so

กำหนดค่า SSL และพร็อกซี่

#extra/httpd-ssl.conf
DocumentRoot "/เว็บไซต์/โฟลเดอร์"
ServerName domain.com

# การกำหนดพร็อกซี
SSLProxyEngine on

ProxyRequests Off
ProxyPass /wss ws://127.0.0.1:8282/wss
ProxyPassReverse /wss ws://127.0.0.1:8282/wss

# เพิ่มการสนับสนุนโปรโตคอล SSL, ลบโปรโตคอลที่ไม่ปลอดภัย
SSLProtocol all -SSLv2 -SSLv3
# เปลี่ยนชุดรหัสดังนี้
SSLCipherSuite HIGH:!RC4:!MD5:!aNULL:!eNULL:!NULL:!DH:!EDH:!EXP:+MEDIUM
SSLHonorCipherOrder on
# กำหนดค่าใบรับรองสาธารณะ
SSLCertificateFile /server/httpd/cert/your.pem
# กำหนดค่าใบรับรองส่วนตัว
SSLCertificateKeyFile /server/httpd/cert/your.key
# กำหนดค่าใบรับรอง Chain,
SSLCertificateChainFile /server/httpd/cert/chain.pem

การทดสอบ

// ใบรับรองจะตรวจสอบชื่อโดเมน โปรดเชื่อมต่อด้วยชื่อโดเมน โดยไม่มีพอร์ต
ws = new WebSocket("wss://domain.com/wss");

ws.onopen = function() {
    alert("เชื่อมต่อสำเร็จ");
    ws.send('tom');
    alert("ส่งสตริงหนึ่งไปยังเซิร์ฟเวอร์: tom");
};
ws.onmessage = function(e) {
    alert("ได้รับข้อความจากเซิร์ฟเวอร์: " + e.data);
};

วิธีที่สอง ใช้ Workerman เปิด SSL โดยตรง (ไม่แนะนำ)

หมายเหตุ
การใช้ nginx/apache เป็นพร็อกซี่ SSL และการตั้งค่า SSL ใน Workerman ให้เลือกอย่างใดอย่างหนึ่ง ไม่สามารถเปิดใช้งานพร้อมกันได้。

การเตรียมการ:

  1. Workerman เวอร์ชัน >=3.3.7

  2. PHP ได้ติดตั้งส่วนขยาย openssl

  3. ได้มีการขอใบรับรอง (pem/crt และไฟล์ key) วางในไดเรกทอรีใดก็ได้ในดิสก์

โค้ด:

<?php
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
require_once __DIR__ . '/vendor/autoload.php';

// ใบรับรองควรเป็นใบรับรองที่ขอมา
$context = array(
    // ตัวเลือก ssl เพิ่มเติมโปรดดูในคู่มือ http://php.net/manual/zh/context.ssl.php
    'ssl' => array(
        // กรุณาใช้เส้นทางสัมบูรณ์
        'local_cert'        => 'เส้นทางบนดิสก์/server.pem', // สามารถใช้ไฟล์ crt ได้
        'local_pk'          => 'เส้นทางบนดิสก์/server.key',
        'verify_peer'       => false,
        'allow_self_signed' => true, // หากเป็นใบรับรองที่เซ็นด้วยตนเอง ให้เปิดตัวเลือกนี้
    )
);
// ที่นี่ตั้งค่าเป็นโปรโตคอล websocket (พอร์ตใดก็ได้ แต่อย่าถูกใช้โดยโปรแกรมอื่น)
$worker = new Worker('websocket://0.0.0.0:8282', $context);
// ตั้งค่า transport เปิดใช้งาน ssl, websocket + ssl คือ wss
$worker->transport = 'ssl';
$worker->onMessage = function(TcpConnection $con, $msg) {
    $con->send('ok');
};

Worker::runAll();

ด้วยโค้ดข้างต้น Workerman จะฟังโปรโตคอล wss และไคลเอนต์สามารถเชื่อมต่อกับ workerman ผ่านโปรโตคอล wss เพื่อการสื่อสารที่ปลอดภัยได้。

การทดสอบ

เปิดเบราว์เซอร์ Chrome กด F12 เพื่อเปิดคอนโซลการดีบัก ในช่อง Console ให้พิมพ์ (หรือใส่โค้ดด้านล่างลงในหน้า html แล้วเรียกใช้ด้วย js)

// ใบรับรองจะตรวจสอบชื่อโดเมน โปรดเชื่อมต่อด้วยชื่อโดเมน โดยมีหมายเลขพอร์ต
ws = new WebSocket("wss://domain.com:8282");
ws.onopen = function() {
    alert("เชื่อมต่อสำเร็จ");
    ws.send('tom');
    alert("ส่งสตริงหนึ่งไปยังเซิร์ฟเวอร์: tom");
};
ws.onmessage = function(e) {
    alert("ได้รับข้อความจากเซิร์ฟเวอร์: " + e.data);
};

ข้อผิดพลาดทั่วไป
SSL routines:SSL23_GET_CLIENT_HELLO:http request
สาเหตุเกิดจากไคลเอนต์ใช้ ws://domain.com เพื่อเข้าถึง ซึ่งเป็นที่อยู่ที่ถูกต้องควรเป็น wss://domain.com การเข้าถึงควรเริ่มต้นด้วย wss。
ปัญหานี้มักเกิดจากการที่พอร์ตในตอนแรกเป็น ws แต่เปลี่ยนเป็น wss โดยไม่ได้รีเฟรชเพจของไคลเอนต์ ทำให้ยังคงเข้าถึงในแบบ ws ส่งผลให้เกิดข้อผิดพลาด
สามารถข้ามข้อผิดพลาดนี้ได้ เนื่องจากไม่ส่งผลต่อการเชื่อมต่อ wss ปกติ。

หมายเหตุ:

  1. หากจำเป็นต้องใช้พอร์ต 443 โปรดใช้วิธีการแรกคือการใช้ nginx/apache เป็นพร็อกซี่เพื่อให้บริการ wss।

  2. พอร์ต wss สามารถเข้าถึงได้เฉพาะผ่านโปรโตคอล wss ไม่สามารถเข้าถึงด้วย ws ได้。

  3. ใบรับรองมักจะถูกผูกไว้กับชื่อโดเมน ดังนั้นในการทดสอบให้ไคลเอนต์ใช้การเชื่อมต่อด้วยชื่อโดเมน ไม่ควรใช้ ip ในการเชื่อมต่อ。

  4. หากเกิดปัญหาไม่สามารถเข้าถึงได้ โปรดตรวจสอบไฟร์วอลล์ของเซิร์ฟเวอร์。

  5. วิธีนี้ต้องการ PHP เวอร์ชัน >=5.6 เนื่องจากมินิโปรแกรม WeChat ต้องการ tls1.2 ขณะที่ PHP เวอร์ชันต่ำกว่า 5.6 ไม่รองรับ tls1.2。

บทความที่เกี่ยวข้อง:
ผ่านพร็อกซี่เพื่อรับไคลเอนต์ ip จริง
การตั้งค่า SSL context ของ workerman อ้างอิง