Hello everyone I'm new to swift and I can't quite figure it out yet:( I am developing a simple online game for mac os that involves two players connected to the same WIFI. I need to constantly receive information from the server and I don't understand how to implement it. If I call the receive function indefinitely, then my program freezes. I realized that this should happen asynchronously, but that's just how my program understands when a package came from the server. I understand that I need a delegate or handler, but I don't understand how to do it. Please help me to add the receive function and everything that is necessary for it
import Foundation
import Network
enum CustomErrors: Error {
case DataError
case NetworkError
case DecoderError
case InvalidAddress
}
class TapperConnection: ObservableObject {
private var _serverAlive = false
private var connection: NWConnection!
private var serverPort: UInt16 = 20001
private var serverIp: String = "127.0.0.1"
private var _myDeviceName = Host.current().localizedName ?? ""
@Published var messageDc: [HostData] = []
@Published var messageLobby: [HostData] = []
@Published var messageState: GameData = GameData()
private var buffer = 2048
private var _inputData = ""
private var _outputData = ""
private var _myIp = ""
private var isServer = false
private var isClient = false
var myIp: String {
return _myIp
}
var myDeviceName: String {
return _myDeviceName
}
private func getMyIp() -> String? {
var address: String?
var ifaddr: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&ifaddr) == 0 else { return nil }
guard let firstAddr = ifaddr else { return nil }
for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) {
let interface = ifptr.pointee
let addrFamily = interface.ifa_addr.pointee.sa_family
if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) {
let name = String(cString: interface.ifa_name)
if name == "en0" || name == "en2" || name == "en3" || name == "en4" || name == "pdp_ip0" || name == "pdp_ip1" || name == "pdp_ip2" || name == "pdp_ip3" {
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
&hostname, socklen_t(hostname.count),
nil, socklen_t(0), NI_NUMERICHOST)
address = String(cString: hostname)
}
}
}
freeifaddrs(ifaddr)
return address
}
private func isValidIP(_ ip: String) -> Bool {
let regex = try! NSRegularExpression(pattern: "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")
return regex.firstMatch(in: ip, range: NSRange(location: 0, length: ip.utf16.count)) != nil
}
@Sendable
private func updateServerState(to state: NWConnection.State) {
switch state {
case .setup:
_serverAlive = true
case .waiting:
_serverAlive = true
case .ready:
_serverAlive = true
case .failed:
_serverAlive = false
case .cancelled:
_serverAlive = false
case .preparing:
_serverAlive = false
default:
_serverAlive = false
}
}
func createConnection() throws {
let ip = getMyIp()
if ip != nil {
serverIp = ip!
_myIp = ip!
} else {
throw CustomErrors.NetworkError
}
isServer = true
do {
try connectToServer()
} catch {
throw CustomErrors.NetworkError
}
}
func createConnection(ip: String) throws {
if isValidIP(ip) {
serverIp = ip
} else {
throw CustomErrors.InvalidAddress
}
let _ip = getMyIp()
if _ip != nil {
_myIp = _ip!
} else {
throw CustomErrors.NetworkError
}
isClient = true
do {
try connectToServer()
} catch {
throw CustomErrors.NetworkError
}
}
private func connectToServer() throws {
if isServer {
// ...............
// run server exec
// ...............
}
let _params = NWParameters(dtls: nil, udp: .init())
_params.requiredLocalEndpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(_myIp), port: 20002)
connection = NWConnection(host: NWEndpoint.Host(serverIp), port: NWEndpoint.Port(rawValue: serverPort)!, using: _params)
connection.stateUpdateHandler = updateServerState(to:)
connection.start(queue: .global())
while !_serverAlive {}
do {
try send(message: "im:\(_myDeviceName)")
receive()
} catch {
print("Error sending disconnect message: \(error)")
}
}
func closeConnection() {
do {
try send(message: "dc:\(_myDeviceName)")
} catch {
print("Error sending disconnect message: \(error)")
}
_serverAlive = false
connection.cancel()
}
func send(message: String) throws {
var error = false
connection.send(content: message.data(using: String.Encoding.utf8), completion: NWConnection.SendCompletion.contentProcessed(({ NWError in
if NWError == nil {
print("Data was sent!")
} else {
error = true
}
})))
if error {
throw CustomErrors.NetworkError
}
}
func receive() {
self.connection.receive(minimumIncompleteLength: 1, maximumLength: 65535) { data, _, isComplete, _ in
if isComplete {
if data != nil {
let response: String = String(decoding: data!, as: UTF8.self)
var decodeData: Any
var messageType: MessageType
(decodeData, messageType) = try! Decoder.decodeMessage(response)
switch messageType {
case MessageType.lobby:
self.messageLobby = decodeData as! [HostData]
case MessageType.state:
self.messageState = decodeData as! GameData
case MessageType.dc:
self.messageDc = decodeData as! [HostData]
}
}
self.receive()
}
}
}
}
Looking at your code I see that it’s trying to get the device’s IP address. That’s almost always a problem )-: In Extra-ordinary Networking I have a subpost entitled Don’t Try to Get the Device’s IP Address that has the whole backstory here.
I am developing a simple online game for mac os that involves two players connected to the same [Wi-Fi].
In that situation it’s best for your server to use Bonjour [1]:
-
On the server, advertise your listener using Bonjour. Specifically, set up the listener like this:
let listener = try NWListener(using: .udp) listener.service = .init(type: "_mygame._udp")
where
_mygame
is a service type for your app. -
On the client, browse for listeners using
NWBrowser
. Give it the same service type as you used in the previous step. -
When the user chooses a listener, connect to that using the endpoint returned by the browser.
This gets you out of the business of messing around with IP addresses, which is a good thing because IP addresses are super tricky to use correctly.
I have a simple example of this process in Getting Started with Bonjour. It uses TCP, but the basic process is the same for UDP.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] Bonjour is an Apple term for three industry-standard protocols:
-
RFC 6762 Multicast DNS
-
RFC 6763 DNS-Based Service Discovery