当前位置: > > > Swift - 使用socket进行通信(附聊天室样例)

Swift - 使用socket进行通信(附聊天室样例)

(本文代码已升级至Swift4)

在Swift开发中,如果我们需要保持客服端和服务器的长连接进行双向的数据通信,使用socket是一种很好的解决方案。
下面通过一个聊天室的样例来演示socket通信,这里我们使用了一个封装好的socket库(SwiftSocket)。

SwiftSocket配置: 
(1)将下载下来的源码包中 SwiftSocket.xcodeproj 拖拽至我们的工程中。

(2)工程 -> General -> Embedded Binaries 项,把iOS版的framework添加进来: SwiftSocket.framework

功能如下:
1,程序包含服务端和客服端,这里为便于调试把服务端和客服端都做到一个应用中
2,程序启动时,自动初始化启动服务端,并在后台开启一个线程等待客服端连接
3,同时,客户端初始化完毕后会与服务端进行连接,同时也在后台开启一个线程等待接收服务端发送的消息
4,连接成功后,自动生成一个随机的用户名(如“游客232”)并发送消息告诉服务器这个用户信息
5,点击界面的“发送消息”按钮,则会发送聊天消息到服务端,服务端收到后会把聊天消息发给所有的客服端。客服端收到后显示在对话列表中

注意1:消息传递过程使用的json格式字符串数据。
目前这个demo里消息种类有用户登录消息,聊天消息,后面还可以加上用户退出消息等。为了在接收到消息以后,能判断是要执行什么命令。我们创建消息体的时候使用字典NSDictionary(其中cmd表示命令类型,content表示内容体,nickname表示用户名,等等),发送消息时把字典转成json格式的字符串传递,当另一端接收的时候,又把json串还原成字典再执行响应的动作。

注意2:可变长度消息的发送
由于我们发送的消息长度是不固定的。所以看下面代码可以发现,为了让接受方知道我们这条消息的长度。每次我们要发送一条消息。其实是发两个消息包出去。
第1个包长度固定为4个字节,里边记录的是接下来的实际消息包的长度。
第2个包才是实际的消息包,接受方通过第一个包知道了数据长度,从而进行读取。

效果图如下:
   

代码如下:
--- ViewController.swift 主页面 ---
import UIKit
import SwiftSocket

class ViewController: UIViewController {
    
    //消息输入框
    @IBOutlet weak var textFiled: UITextField!
    //消息输出列表
    @IBOutlet weak var textView: UITextView!
    
    //socket服务端封装类对象
    var socketServer:MyTcpSocketServer?
    //socket客户端类对象
    var socketClient:TCPClient?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //启动服务器
        socketServer = MyTcpSocketServer()
        socketServer?.start()
        
        //初始化客户端,并连接服务器
        processClientSocket()
    }
    
    //初始化客户端,并连接服务器
    func processClientSocket(){
        socketClient=TCPClient(address: "localhost", port: 8080)
        
        DispatchQueue.global(qos: .background).async {
            //用于读取并解析服务端发来的消息
            func readmsg()->[String:Any]?{
                //read 4 byte int as type
                if let data=self.socketClient!.read(4){
                    if data.count==4{
                        let ndata=NSData(bytes: data, length: data.count)
                        var len:Int32=0
                        ndata.getBytes(&len, length: data.count)
                        if let buff=self.socketClient!.read(Int(len)){
                            let msgd = Data(bytes: buff, count: buff.count)
                            if let msgi = try? JSONSerialization.jsonObject(with: msgd,
                                                        options: .mutableContainers) {
                                return msgi as? [String:Any]
                            }
                        }
                    }
                }
                return nil
            }
            
            //连接服务器
            switch self.socketClient!.connect(timeout: 5) {
            case .success:
                DispatchQueue.main.async {
                    self.alert(msg: "connect success", after: {
                    })
                }
                
                //发送用户名给服务器(这里使用随机生成的)
                let msgtosend=["cmd":"nickname","nickname":"游客\(Int(arc4random()%1000))"]
                self.sendMessage(msgtosend: msgtosend)
                
                //不断接收服务器发来的消息
                while true{
                    if let msg=readmsg(){
                        DispatchQueue.main.async {
                            self.processMessage(msg: msg)
                        }
                    }else{
                        DispatchQueue.main.async {
                            //self.disconnect()
                        }
                        //break
                    }
                }
            case .failure(let error):
                DispatchQueue.main.async {
                    self.alert(msg: error.localizedDescription,after: {
                    })
                }
            }
        }
    }
    
    //“发送消息”按钮点击
    @IBAction func sendMsg(_ sender: AnyObject) {
        let content=textFiled.text!
        let message=["cmd":"msg","content":content]
        self.sendMessage(msgtosend: message)
        textFiled.text=nil
    }
    
    //发送消息
    func sendMessage(msgtosend:[String:String]){
        let msgdata=try? JSONSerialization.data(withJSONObject: msgtosend,
                                                options: .prettyPrinted)
        var len:Int32=Int32(msgdata!.count)
        let data = Data(bytes: &len, count: 4)
        _ = self.socketClient!.send(data: data)
        _ = self.socketClient!.send(data:msgdata!)
    }
    
    //处理服务器返回的消息
    func processMessage(msg:[String:Any]){
        let cmd:String=msg["cmd"] as! String
        switch(cmd){
        case "msg":
            self.textView.text = self.textView.text +
                (msg["from"] as! String) + ": " + (msg["content"] as! String) + "\n"
        default:
            print(msg)
        }
    }
    
    //弹出消息框
    func alert(msg:String,after:()->(Void)){
        let alertController = UIAlertController(title: "",
                                                message: msg,
                                                preferredStyle: .alert)
        self.present(alertController, animated: true, completion: nil)
        
        //1.5秒后自动消失
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.5) {
            alertController.dismiss(animated: false, completion: nil)
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}
--- MyTcpSocketServer.swift 服务端 ---
import UIKit
import SwiftSocket

//服务器端口
var serverport:Int32 = 8080

//客户端管理类(便于服务端管理所有连接的客户端)
class ChatUser:NSObject{
    var tcpClient:TCPClient?
    var username:String=""
    var socketServer:MyTcpSocketServer?
    
    //解析收到的消息
    func readMsg()->[String:Any]?{
        //read 4 byte int as type
        if let data=self.tcpClient!.read(4){
            if data.count==4{
                let ndata = NSData(bytes: data, length: data.count)
                var len:Int32=0
                ndata.getBytes(&len, length: data.count)
                if let buff=self.tcpClient!.read(Int(len)){
                    let msgd = Data(bytes: buff, count: buff.count)
                    let msgi = (try! JSONSerialization.jsonObject(with: msgd,
                                options: .mutableContainers)) as! [String:Any]
                    return msgi
                }
            }
        }
        return nil
    }
    
    //循环接收消息
    func messageloop(){
        while true{
            if let msg=self.readMsg(){
                self.processMsg(msg: msg)
            }else{
                self.removeme()
                break
            }
        }
    }
    
    //处理收到的消息
    func processMsg(msg:[String:Any]){
        if msg["cmd"] as! String=="nickname"{
            self.username=msg["nickname"] as! String
        }
        self.socketServer!.processUserMsg(user: self, msg: msg)
    }
    
    //发送消息
    func sendMsg(msg:[String:Any]){
        let jsondata=try? JSONSerialization.data(withJSONObject: msg,
                                                 options:.prettyPrinted)
        var len:Int32=Int32(jsondata!.count)
        
        let data = Data(bytes: &len, count: 4)
        _ = self.tcpClient!.send(data: data)
        _ = self.tcpClient!.send(data: jsondata!)
    }
    
    //移除该客户端
    func removeme(){
        self.socketServer!.removeUser(u: self)
    }
    
    //关闭连接
    func kill(){
        _ = self.tcpClient!.close()
    }
}

//服务端类
class MyTcpSocketServer: NSObject {
    var clients:[ChatUser]=[]
    var server:TCPServer=TCPServer(address: "127.0.0.1", port: serverport)
    var serverRuning:Bool=false
    
    //启动服务
    func start() {
        _ = server.listen()
        self.serverRuning=true
        
        DispatchQueue.global(qos: .background).async {
            while self.serverRuning{
                let client=self.server.accept()
                if let c=client{
                    DispatchQueue.global(qos: .background).async {
                        self.handleClient(c: c)
                    }
                }
            }
        }
        
        self.log(msg: "server started...")
    }
    
    //停止服务
    func stop() {
        self.serverRuning=false
        _ = self.server.close()
        //forth close all client socket
        for c:ChatUser in self.clients{
            c.kill()
        }
        self.log(msg: "server stoped...")
    }
    
    //处理连接的客户端
    func handleClient(c:TCPClient){
        self.log(msg: "new client from:"+c.address)
        let u=ChatUser()
        u.tcpClient=c
        clients.append(u)
        u.socketServer=self
        u.messageloop()
    }
    
    //处理各消息命令
    func processUserMsg(user:ChatUser, msg:[String:Any]){
        self.log(msg: "\(user.username)[\(user.tcpClient!.address)]cmd:"+(msg["cmd"] as! String))
        //boardcast message
        var msgtosend=[String:String]()
        let cmd = msg["cmd"] as! String
        if cmd=="nickname"{
            msgtosend["cmd"]="join"
            msgtosend["nickname"]=user.username
            msgtosend["addr"]=user.tcpClient!.address
        }else if(cmd=="msg"){
            msgtosend["cmd"]="msg"
            msgtosend["from"]=user.username
            msgtosend["content"]=(msg["content"] as! String)
        }else if(cmd=="leave"){
            msgtosend["cmd"]="leave"
            msgtosend["nickname"]=user.username
            msgtosend["addr"]=user.tcpClient!.address
        }
        for user:ChatUser in self.clients{
            //if u~=user{
            user.sendMsg(msg: msgtosend)
            //}
        }
    }
    
    //移除用户
    func removeUser(u:ChatUser){
        self.log(msg: "remove user\(u.tcpClient!.address)")
        if let possibleIndex=self.clients.index(of: u){
            self.clients.remove(at: possibleIndex)
            self.processUserMsg(user: u, msg: ["cmd":"leave"])
        }
    }
    
    //日志打印
    func log(msg:String){
        print(msg)
    }
}
源码下载:hangge_756.zip
评论31
  • 31楼
    2018-09-04 18:16
    Ronald

    为什么我在模拟器上可以连接上服务器,而在真机测试始终提示socketerror:connectionTimeout 超时

    站长回复

    模拟器可以的话说明代码是对的,真机不行应该是你那边网络环境问题,这个只能你再检查下来。

  • 30楼
    2018-02-05 17:10
    zhang

    本来用楼主的demo已经做出来了,但是公司有要求SSL加密,无奈换成CocoaAsyncSocket。关于28楼的问题,以CocoaAsyncSocket的使用经验,只要不彻底中断app,socket就会一直保持连接,但是从待机状态变成活跃状态之后,登录状态没了,所以每次app重新变成活跃状态时,我都是重新调一次登录接口(首次登录,服务器返回一个token参数,之后用token重新登录),登录成功就说明还连着。

    站长回复

    谢谢你分享的经验,对我来说也是很有帮助的。

  • 29楼
    2018-01-31 09:33
    特菜

    大神好 我只找着 tvOS 和 mac 版本的 SwiftSocket.framework
    没看见 iphone 版本的 不知道是否有哪里出错了

    站长回复

    确实是有iOS版的呀。你再重新下载个试试呢。

  • 28楼
    2018-01-02 14:50
    LL

    您好。想请问您是否有做过判断swiftsocket中判断套接字是否还连接的功能? 最近在搞这个,需要检测重连,但是找了很多资料也不知道如何判断socket是否还与服务端连接着 哭哭

    站长回复

    这个一般都是通过“socket心跳包机制”解决,你可上网搜一下相关内容。

  • 27楼
    2017-12-21 18:28
    愤怒的程序员

    大神能加个qq不

    站长回复

    不好意思啊,QQ只加身边的人。不过你可在网站上给我留言,我都会看到的。

  • 26楼
    2017-12-17 15:30
    zhang

    大神,看了您的DEMO,socket已经连接成功了,但是项目又做了SSL加密,研究一天好无头绪,请问有没有什么思路能指点一下,谢谢。

    站长回复

    基于ssl的socket通讯我也没做过,暂时帮不了你了。

  • 25楼
    2017-12-12 10:00
    qaz

    楼主您好,请问这个socket编程收到服务端新消息有回调方法吗

    站长回复

    不明白你的意思,socket编程的话,客户端这边是在while循环里面不断地接收服务端发过来的消息(不会跳出循环)

  • 24楼
    2017-11-06 12:58
    y

    这个好像不能发送大文件
    while (len - byteswrite > 0) {
    int writelen = (int)write(socketfd, data + byteswrite, len - byteswrite);
    if (writelen < 0) {
    return -1;
    }
    sendsize == Int32(data.count)
    sendSize = -1, 所以总是发送失败

    站长回复

    我测试了下大文件也是没问题的啊,你用的是最新版本的SwiftSocket吗?

  • 23楼
    2017-11-03 15:38
    卡分其

    刚下载运行了一下,这个代码报错,运行不了。
    int retval = select(sockfd + 1,NULL, &fdwrite, NULL, &tvSelect);

    站长回复

    Swift4语法有些变化,我把文章代码更新了,你可以再看下。

  • 22楼
    2017-07-25 20:16
    唐50

    怎么能实现局域网的聊天呢,这个好像只能实现一台电脑开多个模拟器然后相互聊天

    站长回复

    局域网聊天的话应该是有一台服务器,然后每个设备都与这个服务器进行socket通信。某个设备发出消息后,服务器将消息转发给其他的设备。

  • 21楼
    2017-05-05 16:22
    古月

    请问 ?ysocket.c 中的fd 是代表什么来着

    站长回复

    这个我也没研究过,不太清楚。

  • 20楼
    2017-05-02 09:01
    Ann

    航哥你好! 我服务器端是一次性发过来的,客户端收到以0309开头\n结尾的数据之后就跳出循环,不在接收服务器的数据了,直到再次请求服务器在接收。我发现服务器发过来的速度比客户端接收的要快,我在接收的时候sleep了一下,是不是因为客户端处理的太慢数据丢了,就死在循环里了呢? 就算是数据丢了,能不能有什么办法跳出循环在从新请求服务器呢?谢谢!!

    站长回复

    1,服务器发送过来的数据很大吗?理论上客户端不可能会处理太慢而丢数据的。
    2,你打印下客户端收到的数据,看看都是开头部分有,结尾部分没有吗?
    3,如果想要数据不正确重新请求,试试看添加下面红色部分的代码。
    //读取数据
    //不断接收服务器发来的消息
    while true{
        if let msg=readmsg(){
            //是否有后缀
            if msg.hasPrefix("0309") && msg.hasSuffix("\n") {
                //print(msg)
                self.processMessage(msg)
                break
            }
            //不匹配的话重新请求
            else{
              self.processClientSocket()
              break
            }
        }else{
            print("接收失败")
            break
        }
    }

  • 19楼
    2017-04-28 17:19
    Ann

    //初始化客户端,并连接服务器
    func processClientSocket(){

    //用于读取并解析服务端发来的消息-----字符串
    func readmsg()->String?{
    sleep(sleepTime)
    if let buff=socketClient!.readAll(){

    let msgi:String = String(bytes: buff, encoding: String.Encoding.utf8)!
    //print(msgi)
    return msgi
    }
    return nil
    }

    if isConnect
    {
    //发送数据
    let msgtosend = "newshouji££|03\(sjkdm)|\(shidm)\(qudm)\(dwdm1)\(dwdm2)|0309|\(count)"
    let (success, errmsg) = socketClient!.send(str: msgtosend)
    if success
    {
    //读取数据
    //不断接收服务器发来的消息
    while true{
    if let msg=readmsg(){
    //是否有后缀
    if msg.hasPrefix("0309") && msg.hasSuffix("\n") {
    //print(msg)
    self.processMessage(msg)
    break
    }
    }else{
    print("接收失败")
    break
    }
    }
    }else {
    let alert = UIAlertController(title: "提示", message: errmsg, preferredStyle: UIAlertControllerStyle.alert)

    let yes = UIAlertAction(title: "确定", style: UIAlertActionStyle.default, handler: nil)

    alert.addAction(yes)
    self.present(alert, animated: true, completion: nil)
    print(errmsg)
    }
    }
    else {
    let alert = UIAlertController(title: "提示", message: "网络连接异常!", preferredStyle: UIAlertControllerStyle.alert)

    let yes = UIAlertAction(title: "确定", style: UIAlertActionStyle.default, handler: nil)

    alert.addAction(yes)
    self.present(alert, animated: true, completion: nil)
    }

    }
    航哥,这个是我在你的基础上改,没用线程,我就是在登录页连的服务器, 程序要有好多个页面,我是用个while true不断接收服务器的消息,直到头尾都对,退出循环,现在就有个问题,有时候收的不对,就死在循环里面了,就是收的不对要怎么跳出循环呢,我不知道了,求教!谢谢!!

    站长回复

    不知道你服务端代码,还有业务逻辑是怎么样的,目前还不好定位问题原因。我先确认几个地方:

    1,我看你客户端这边代码。收到消息后,如果消息头是0309,消息尾是\n则进行处理。比如:0309xxxxxxxxxxxxxxx\n。那这个消息服务端是一次性发过来呢,还是分多次发送(比如先发送0309xxxxxx,再发送xxxxxxxxx\n)。
    2,客户端这边是不是只要接受到一段完整的消息后,比如:0309xxxxxxxxxxxxxxx\n。就退出循环,不再接受服务端后续发送过来的消息了。

  • 18楼
    2017-04-05 11:55
    字符串

    航哥,你这个不支持ipv6,我能直接在上面修改吗,然后支持ipv6。

    站长回复

    当然可以。如果你修改成功的话,不妨发我一份,我在网站上分享出来。

  • 17楼
    2017-03-27 14:07
    字符串

    是否支持ipv6吗?

    站长回复

    这个还没测试过。

  • 16楼
    2017-03-26 11:13
    lll

    进入你的链接里 咋没找到那个封装库

    站长回复

    有的啊,这个链接就是它的GitHub主页,在上面可以下载整个库。

  • 15楼
    2016-08-23 11:12
    木槿

    我这个是和仪器的WIFI模块通讯,我想改成直接send(0xAA,0xAA,0x00,0x12)给仪器wifi模块的c服务端发送这样的指令,并收到回复。这个该怎么改啊?

    站长回复

    给设备发送数据我还没做过,这个我暂时帮不了你了。

  • 14楼
    2016-08-10 11:54
    linjoe

    站长,想咨询一个问题,我用GCDAsyncSocket写的tcp,但是发送数据怎么也看不明白,贴一下代码您看看怎么回事
    func Cilent(host: String) {
    do {
    var test = [ "title": "data" ]
    clientSocket = GCDAsyncSocket()
    clientSocket.delegate = self
    clientSocket.delegateQueue = dispatch_get_global_queue(0,0)
    try clientSocket.connectToHost (host, onPort: 50000, withTimeout: 5000)
    clientSocket.writeData(test["title"], withTimeout: 20, tag: Int)
    clientSocket.readDataToData(data, withTimeout: 20, tag: Int)
    }
    catch {
    print("error")
    }
    }

    站长回复

    GCDAsyncSocket我还没用过,暂时帮不了你了。

  • 13楼
    2016-08-05 21:49
    ITMoney

    站长,后面服务端的代码能直接放在服务器上么,不是应该用PHP、Node.js之类的么?还有我要做一个验证用户账号登录注册的服务器该怎么写?谢谢~

    站长回复

    这个只是个演示,你如果使用Mac电脑作为服务器,当然可以使用Swift写个socket服务端程序。

    帐号验证那个无非就是客户端把表单数据提交到服务器,服务器处理后返回响应结果。这个和本文socket就没什么关系了。

  • 12楼
    2016-08-02 19:22
    丿心梦无痕

    改了改还是不行,代码一开始是少4个字节,我改了改还是不行,少开头一个字节
    链接服务器成功-->ELCOME 192.168.0.131:627

    链接服务器成功-->K

    链接服务器成功-->SG:1470136693458

    链接服务器成功-->SG:1470136694459

    链接服务器成功-->SG:1470136695459

    链接服务器成功-->SG:1470136696459

    应该是::WELCOME
    MSG
    OK

    站长回复

    你这是服务器发送过来的数据吧。目前样例客户端是先读取前面4个字节的数据,这里面存的是后面消息正文的长度。然后接下来再根据前面获取到的长度来读取内容。

    也就是说每一条消息,服务端是发两次包。而客户端这边也是读取两次。
    如果你服务端每次只发一个包(即直接发消息正文过来),客户端这边仍然读取两次,那么正文实际上就从第五个字节开始,自然会有问题。

  • 11楼
    2016-08-02 15:27
    不了了之

    航哥,请教一下,我现在要用Cordova开发一个聊天功能的插件,但是这个聊天功能我可以使用Socket实现吗?谢谢!

    站长回复

    这么实现当然没问题。

  • 10楼
    2016-07-25 17:29
    k12

    站长,如果用os x做的话,会不会有些不同?

    站长回复

    这个没试过,不过如果都用Swift开发的话,我想大体上是差不多的吧。

  • 9楼
    2016-07-21 17:25
    GC

    请问要让两个使用者连结该从哪修改呢?

    站长回复

    你指的是一对一的连接吗,那就一个启动服务端,一个启动客户端进行连接。

  • 8楼
    2016-07-17 10:08
    我爱航哥

    航哥,能不能稍微改改这个代码,直接收发String类型。多谢航哥 :D

    站长回复

    这个不改了,实际使用中每次只发个字符串不够用,还是字典方便(除非你把所有信息拼成一条字符串,那边收到再拆开)

    不过多的可以当少的用,你传递的字典里可以只放一个键值对,value值就放你要传递的字符串。

  • 7楼
    2016-07-15 18:00
    chiu

    您好 幫了很大的忙
    想請問要連socket server是其他語言的 ip 跟port要改哪裡呢謝謝

    站长回复

    你的意思是服务端使用其它语言写的吗?只要把客户端这边链接服务的ip、端口改下就好了(在ViewController.swift 中的第28行)

  • 6楼
    2016-05-27 11:49
    gtkrockets

    我的服务端socket是其它语言的,按照传输格式,先发需要读区的长度,然后按长度读取,可是swift self.tcpClient!.read(Int(len)) (稍微多一点数据的时候)读取出来并不是我发送的长度,有时成功,有时失败。请问是为什么?

    站长回复

    这个具体可能的原因我也不太清楚,暂时帮不了你了。

  • 5楼
    2016-05-11 11:02
    wangjun

    我用了,如果是本地的服务器 addr: "127.0.0.1", port: 5222,//服务器端口var serverport = 5222 这三个地方改一下就能成功!!谢谢楼主!

    站长回复

    不客气,能用就好:)

  • 4楼
    2016-05-03 15:24
    luotao

    你好,我用的是xcode 7.2.1 我编译之后发现错误:

    Unknown attribute '_silgen_name' Expected declaration;错误,请问如何解决?

    站长回复

    我Xcode版本是7.3,刚运行了下没发现有这个错误。我也不知道是什么情况了。

  • 3楼
    2016-04-05 11:57
    sun

    为什么swiftsocket 报错

    站长回复

    我又测试了下,是没问题的啊

  • 2楼
    2016-04-04 19:02
    朱朱朱

    请问这个用到了滑动窗口协议吗? 能不能提供一些关于滑动窗口协议的swift编程资料

    站长回复

    滑动窗口协议我也没研究过,所以暂时帮不上你了。

  • 1楼
    2016-03-30 16:11
    海鑫

    大神.我用这段代码,链接不上服务器

    站长回复

    swift版本更新造成语法改变了,现已修正。