You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS与Python App Engine中Socket.IO的HTTPS/SSL连接问题求助

GCP App Engine Flask-SocketIO 与 iOS Socket.IO 连接问题及证书配置方案

问题背景

本地部署Python 3.7+Flask-SocketIO服务与Swift开发的iOS应用时,Socket.IO可正常使用WebSocket;但部署到GCP App Engine后,连接始终无法升级为WebSocket,默认使用长轮询,推测为HTTPS/SSL相关问题。

两端代码示例

App Engine 服务端代码

app = Flask(__name__)
app.config["SECRET_KEY"] = socket_secret
socketio = SocketIO(app, cors_allowed_origins="*")

@socketio.event()
def connect():
    print("******* connection established *******")

@socketio.on("update")
def update(json):
    handle_update(json)

if __name__ == '__main__':
    print("Starting")
    socketio.run(app, debug=True)

iOS 客户端代码

private lazy var socketManager: SocketManager = {
    let config: SocketIOClientConfiguration = [.log(true),
                                               .compress,
                                               //.connectParams(["ssl_verify": false]),
                                               //.forceWebsockets(true)
    ]
    let manager = SocketManager(socketURL: URL(string: domain)!, config: config)
    return manager
}()

问题1:App Engine 配置 SSL 并适配 Socket.IO WebSocket

App Engine 无需手动创建和配置SSL证书给Socket.IO实例,因为平台自带SSL终止机制:前端负载均衡会处理HTTPS请求,转发给后端的是HTTP请求。WebSocket无法升级的核心问题在于环境配置,而非证书本身,解决方案如下:

  1. 指定异步模式
    修改SocketIO初始化代码,添加async_mode="eventlet"(App Engine标准环境推荐使用eventlet作为异步引擎):

    socketio = SocketIO(app, cors_allowed_origins="*", async_mode="eventlet")
    
  2. 配置app.yaml启用WebSocket
    在项目根目录的app.yaml中添加WebSocket支持的路由配置,并强制HTTPS:

    runtime: python37
    entrypoint: gunicorn -k eventlet -w 1 main:app
    handlers:
    - url: /socket.io/
      script: auto
      secure: always
      redirect_http_response_code: 301
      websocket: true
    - url: /.*
      script: auto
      secure: always
      redirect_http_response_code: 301
    

    其中websocket: true是启用WebSocket的关键配置。

  3. 自定义域名SSL配置
    若使用自定义域名,在GCP控制台的App Engine > 设置 > 自定义域名中添加域名,GCP会自动为其颁发免费的Let's Encrypt SSL证书,无需手动操作证书创建和部署。


问题2:iOS端SocketManager实现证书固定

要实现类似NSURLConnection的证书固定,需将服务器的证书(或公钥)嵌入应用,并在Socket.IO的网络请求中验证证书,步骤如下:

  1. 嵌入证书到项目
    导出服务器的SSL证书(PEM格式),将证书文件添加到iOS项目中,并确保在Build Phases > Copy Bundle Resources中包含该文件。

  2. 实现自定义URLSessionDelegate
    编写证书验证逻辑,仅信任嵌入的证书:

    class SocketSSLDelegate: NSObject, URLSessionDelegate {
        func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
            guard let serverTrust = challenge.protectionSpace.serverTrust else {
                completionHandler(.cancelAuthenticationChallenge, nil)
                return
            }
            
            // 加载本地证书
            guard let certPath = Bundle.main.path(forResource: "your_server_cert", ofType: "pem"),
                  let certData = try? Data(contentsOf: URL(fileURLWithPath: certPath)),
                  let localCert = SecCertificateCreateWithData(nil, certData as CFData) else {
                completionHandler(.cancelAuthenticationChallenge, nil)
                return
            }
            
            // 配置信任策略,仅信任本地证书
            let sslPolicy = SecPolicyCreateSSL(true, challenge.protectionSpace.host as CFString)
            SecTrustSetPolicies(serverTrust, [sslPolicy] as CFArray)
            SecTrustSetAnchorCertificates(serverTrust, [localCert] as CFArray)
            SecTrustSetAnchorCertificatesOnly(serverTrust, true)
            
            // 验证信任结果
            var trustResult: SecTrustResultType = .invalid
            SecTrustEvaluate(serverTrust, &trustResult)
            
            if trustResult == .unspecified || trustResult == .proceed {
                let credential = URLCredential(trust: serverTrust)
                completionHandler(.useCredential, credential)
            } else {
                completionHandler(.cancelAuthenticationChallenge, nil)
            }
        }
    }
    
  3. 配置SocketManager使用自定义URLSession
    在SocketManager初始化时,传入带有自定义Delegate的URLSession:

    private lazy var socketManager: SocketManager = {
        let sslDelegate = SocketSSLDelegate()
        let customSession = URLSession(configuration: .default, delegate: sslDelegate, delegateQueue: nil)
        
        let config: SocketIOClientConfiguration = [
            .log(true),
            .compress,
            .session(customSession),
            .forceWebsockets(true) // 可选,强制使用WebSocket协议
        ]
        
        guard let socketURL = URL(string: domain) else {
            fatalError("Invalid domain URL")
        }
        return SocketManager(socketURL: socketURL, config: config)
    }()
    

注意:若使用GCP默认的*.appspot.com域名,其证书会定期自动更新,建议固定证书公钥而非整个证书,避免证书过期导致应用无法连接。


内容的提问来源于stack exchange,提问作者Magoo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 01:15:38