Date
1 - 9 of 9
Understanding NSNetService and peer-to-peer streaming
Graham Cox
Hi all,
I’m trying to add a peer-to-peer communication feature to a game I’m developing. So far it’s half working, which given the paucity of documentation, I’m actually quite proud of… Because it’s peer-to-peer, any app can act as both client and server, so getting straight which bit is doing what is quite hard. But for the sake of discussion, I’ll call the service that initiates a connection the server, and the app that receives it the client. I’m using NSNetService and NSNetServiceBrowser to act as an interface to Bonjour to discover other copies of my game on the network. This part works well - I can advertise myself as a service, and I can receive notifications of other identical services. The ’server' NSNetService receives a -netService:didAcceptConnectionWithInputStream:outputStream:, and schedules those streams in the main run loop and opens them. The ‘server’ also implements -netServiceBrowser:didFindService:moreComing: to wrap the NSNetService representing the client and its streams in a ‘remote player’ object for that client. I can use the ‘remote player’ object to send data to the client via the streams associated with its NSNetService. The client receives the data OK, as the stream delegate’s -stream:handleEvent: So far, so good. But now I need to return data to the ‘server’ peer. The problem is, I don’t know which stream I need to use to do that. When the ‘server’ accepts a connection, it is handed two streams, which are presumably the ones to use. The problem I have is that these streams are just naked streams - there is no associated information that will tell me which ‘client’ they are connected to. At the moment I just schedule these streams in a run loop and open them, and set my general ‘server’ to be their delegate, which is how I am able to receive the data. But given that there will be a pair of these for every connected server, I don’t know which stream to use to return data to a specific server. I have the idea that what I should do is to wrap the stream pair in another object so I can keep them separate, but that still leaves the problem of identifying that object as being a particular server. I can send that identifying information over the stream the first time I use it, and the ‘client’ can then add that information to the server object, but that seems like a kludge - it is relying on my private protocol to pass information that should already be known to the basic NSNetService infrastructure. If you’ve followed this, kudos to you. It’s hard to keep straight who is the client and who is the server, to be honest. But even with that muddle, I feel I’m missing something. If anyone has done something like this successfully and can offer any insights, I’d be most grateful. —Graham |
|
Quincey Morris
On Aug 16, 2017, at 21:24 , Graham Cox <graham@...> wrote:
I recommend you ask this question in the hated-by-some developer forums (forums.developer.apple.com). Quinn the Eskimo fields questions there most days, and he’s exactly the person to give you the answer. (You might find him on the Apple listserver list for networking too, but I don’t subscribe there.) |
|
Right. You need to keep track of that yourself, like with a Peer class that can hold onto those streams and act as their delegate.
NSNetService doesn't really have anything to do with those streams. All Bonjour does is service discovery: it can advertise a service you're running on some port, and it can find other services of a given type. When it resolves a service it gets the IP address and port number. But that's all Bonjour does. When you ask an NSNetService to open a connection, it just resolves the service and then calls an NSStream factory method to open a TCP socket to that IP address / port. Then it returns you the resulting streams. Its job is done. This is just a convenience method that was added later on to simplify connecting to services. Once you've opened the streams, you can remember the name of the service that you got them from. Or you can have a protocol that sends the peer name or UUID or whatever over the socket when it opens; it's up to you. —Jens PS: What you're implementing sounds exactly like what the Multipeer Connectivity framework does. Are you aware of it? You could probably save yourself a ton of work by using it. |
|
Graham Cox
On 18 Aug 2017, at 9:03 am, Graham Cox <graham.cox@...> wrote:PS: What you're implementing sounds exactly like what the Multipeer Connectivity framework does. Are you aware of it? You could probably save yourself a ton of work by using it.No, I’ve not heard of it. I did google for possible solutions, but it invariably finds a bunch of Stackoverflow questions, mostly without useful answers. I’ll check it out now I have a name to go by, though actually I’ve solved 90% of my problem and the remaining 10% is clear enough now. This is a follow-up to my question last week. I got my NSNetService + Streams approach more or less working, with one small problem (i’ll get to that). But I also checked out Multipeer Connectivity (MC) and rolled a solution around that as well. The problem I’m running into with MC is that of testing it. While I have multiple devices to test it on, it’s very inconvenient to do that. I’d far rather test it on a single machine if possible. With the NSNetService design, it was quite happy to ‘see’ itself as another peer on the network, and allow a connection, making testing it on the one machine straightforward. MC seems to go out of its way to prevent that, which on the surface makes sense, because in production you DON’T want your own machine to show up as a remote peer. But it makes testing it really hard. To try and work around it, I tried browsing and advertising using two different MCPeerID objects, with different names. That appears to work, in that the browser sees the advertised service on the same machine as another peer. Unfortunately, it completely fails after the discovery phase to actually connect. So I can test the discovery phase, but not the real meat of the thing, which is the communication. Does anyone have any experience of MC and know how this can be made to work, or what are good approaches to testing, preferably on a single machine? At this point the NSNetService design is functioning to a greater degree, because I can connect, even on the same machine. But the problem I’m having there is because it is stream based, I’m finding it difficult to implement something like MC’s message-based approach to communicating. If I send two ‘messages’ too quickly, they get concatented on the stream, and the receiver can’t tell where the boundary is between the two (or more) messages. So if I can solve that problem, it’s likely that the non-MC design will be the one to use, because it actually works in all other respects. So, how to separate messages that just end up as a stream of bytes? I was thinking I could precede each one with a length field, which seems the simplest idea, assuming that I can be sure I can align the received stream to some sort of ‘frame’, so I can be sure the length field is where I think it is. But if you have any other clever ways, please share! —Graham |
|
Jack Brindle
One suggestion for testing on a single machine is to use VMware Fusion to create multiple macOS VMs. You will want a system with decent speed, but the environment should do exactly what you need with just one physical system.
toggle quoted message
Show quoted text
Note that the current version of Fusion does not yet support High Sierra, so you will need to limit testing to Sierra and below. - Jack On Aug 20, 2017, at 7:00 PM, Graham Cox <graham@...> wrote:On 18 Aug 2017, at 9:03 am, Graham Cox <graham.cox@...> wrote:PS: What you're implementing sounds exactly like what the Multipeer Connectivity framework does. Are you aware of it? You could probably save yourself a ton of work by using it.No, I’ve not heard of it. I did google for possible solutions, but it invariably finds a bunch of Stackoverflow questions, mostly without useful answers. I’ll check it out now I have a name to go by, though actually I’ve solved 90% of my problem and the remaining 10% is clear enough now. |
|
Alex Zavatone
On Aug 20, 2017, at 9:10 PM, Jack Brindle <jackbrindle@...> wrote:This. This. This. This. Create a VM of your current OS. Duplicate it as needed and make changes to each OS image to suit your needs. It’s a massive timesaver with fast SSDs. |
|
This is a classic newbie networking mistake. (No offense I hope!) TCP is a stream, and there is absolutely no delimiter between writes, and no association between the number of bytes written vs. the number read at the receiver. It's true that if you only intermittently send bytes, the receiver will usually receive the same number of bytes from its read call … but not always, depending on circumstances, because the data can get broken up at arbitrary locations. To send and receive messages you have to delimit them somehow. The classic ways are to prefix every message with a count, or to have a reserved byte sequence that denotes end-of-message. (Pascal strings and C strings, anyone?) The former is better as long as you know the message length ahead of time. So for example, on the sending end just write a 32-bit byte count (big- or little-endian, pick one), then the message. On the receiving end, have a resizable read buffer (an NSMutableData works well) and keep reading bytes into it, growing if necessary. After each read decode the message length at the start, then check if you have that many more bytes in the buffer; if so, hand the message to the higher-level code, then delete the count and the message from the buffer, so what remains starts at the next message boundary. Then check if you've got another complete message, and when you don't, issue another read. I have a protocol/framework called BLIP that's basically this to the nth power. It does message queueing, multiplexing, high/low priority, message headers (like HTTP), etc. Might be overkill for your needs, but check it out. —Jens |
|
Graham Cox
On 22 Aug 2017, at 2:46 am, Jens Alfke <jens@...> wrote:On Aug 20, 2017, at 7:00 PM, Graham Cox <graham@...> wrote:This is a classic newbie networking mistake. (No offense I hope!) None at all! I have very little experience in this area. TCP is a stream, and there is absolutely no delimiter between writes, and no association between the number of bytes written vs. the number read at the receiver. It's true that if you only intermittently send bytes, the receiver will usually receive the same number of bytes from its read call … but not always, depending on circumstances, because the data can get broken up at arbitrary locations.Great, I was hoping someone would help me in exactly this way. I thought the length field approach would work, but those with more experience may steer me in a better direction. Good to know I was thinking along the right lines. The data I’m sending is actually a plist. I am using [NSPropertyListSerialization writePropertyList:toStream:…], which is very convenient. But call it twice and the other end gets confused. It surprised me a little bit because I would have thought that having provided such a convenient method for writing a plist to a stream, the apparently equally convenient inverse method would be able to sort this problem out for itself. But no, it appears not. Which makes me wonder in what case it is useful. I have a protocol/framework called BLIP that's basically this to the nth power. It does message queueing, multiplexing, high/low priority, message headers (like HTTP), etc. Might be overkill for your needs, but check it out.I definitely will! many thanks, —Graham |
|
Graham Cox
On 21 Aug 2017, at 12:12 pm, Alex Zavatone <zav@...> wrote: Yes, this makes total sense. I guess I just had a blonde moment (I’m not even blonde) and didn’t think of it. I do use VMWare for testing, though unfortunately I need to pay for the upgrade to support Sierra. —Graham |
|