iOS与Python App Engine中Socket.IO的HTTPS/SSL连接问题求助
问题背景
本地部署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无法升级的核心问题在于环境配置,而非证书本身,解决方案如下:
指定异步模式
修改SocketIO初始化代码,添加async_mode="eventlet"(App Engine标准环境推荐使用eventlet作为异步引擎):socketio = SocketIO(app, cors_allowed_origins="*", async_mode="eventlet")配置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的关键配置。自定义域名SSL配置
若使用自定义域名,在GCP控制台的App Engine > 设置 > 自定义域名中添加域名,GCP会自动为其颁发免费的Let's Encrypt SSL证书,无需手动操作证书创建和部署。
问题2:iOS端SocketManager实现证书固定
要实现类似NSURLConnection的证书固定,需将服务器的证书(或公钥)嵌入应用,并在Socket.IO的网络请求中验证证书,步骤如下:
嵌入证书到项目
导出服务器的SSL证书(PEM格式),将证书文件添加到iOS项目中,并确保在Build Phases > Copy Bundle Resources中包含该文件。实现自定义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) } } }配置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

