Movatterモバイル変換


[0]ホーム

URL:


mikeash.com: just this guy, you know?
Home
Book
The Complete Friday Q&A, advanced topics in Mac OS X and iOS programming.
Blog
GitHub
My GitHub page, containing various open-source libraries for Mac and iOS development, and some miscellaneous projects
Glider Flying
HD Ridge Running Video
List of Landouts
Day in the Life
Skyline Soaring Club
Soaring Society of America
Getting Answers
Ten simple points to follow to get good answers on IRC, mailing lists, and other places
Miscellaneous Pages
Miscellaneous old, rarely-updated content
mike@mikeash.com
E-mail me

Posted at 2014-03-14 13:52 |RSS feed (Full text feed) |Blog Index
Next article:Friday Q&A 2014-05-09: When an Autorelease Isn't
Previous article:Tales From The Crash Mines: Issue #1
Tags:fridayqnanetworkingsockets
Friday Q&A 2014-03-14: Introduction to the Sockets API
byMike Ash  

Network programming is everywhere these days. It's tough to find an iPhone app thatdoesn't use the network for something. We also spend a lot of our time abstracted away from the details of networking, using high-level APIs likeNSURLConnection. Today I'm going to walk you through the lowest-level API you can find without diving into the kernel, the POSIX sockets API.

Sockets
Asocket, as far as this API is concerned, is a kind of UNIX file descriptor. That is to say, it's a (generally small)int that corresponds to a kernel structure. Like any file descriptor, you canread andwrite with it, but sockets also allow other operations.

To create a socket, call thesocket function. It takes three arguments. The first argument is the domain of the socket, which is basically which IP-level protocol the socket will use. Common values for this parameter areAF_INET, which specifies IPv4, andAF_INET6, which specified IPv6. The second argument is the type of the socket, which essentially allows you to choose TCP or UDP. PassSOCK_STREAM for TCP, andSOCK_DGRAM for UDP. The third parameter is more or less vestigial for normal uses these days, and you can just pass zero. Here's a line of code that creates a socket:

ints=socket(AF_INET6,SOCK_STREAM,0)

As is traditional, I will be mostly omitting error checking from my examples in this article. Don't omit it in your code.

This newly-created socket is not very useful on its own. It doesn't actually go anywhere, so you can't read or write. Using it requires more specialized calls. Those calls requireaddresses.

Addresses
Addresses are represented with a family ofstructs which are organized in a way that resembles object-oriented inheritance, if it was badly implemented in C.

The "base class" isstruct sockaddr, which contains the address family (essentially, what kind of address this is) plus raw address data and the overall length:

structsockaddr{__uint8_tsa_len;/* total length */sa_family_tsa_family;/* [XSI] address family */charsa_data[14];/* [XSI] addr value (actually larger) */};

Being only sixteen bytes long, this really doesn't have enough space for all uses, especially considering IPv6. This is not a problem when casting pointers to pass around, but it's troublesome when declaring a localstruct sockaddr toreceive an address from a function. To remedy this problem, there's a newer (as in, only 15 or so years old) "base class" struct with more storage called, appropriately,struct sockaddr_storage:

structsockaddr_storage{__uint8_tss_len;/* address length */sa_family_tss_family;/* [XSI] address family */char__ss_pad1[_SS_PAD1SIZE];__int64_t__ss_align;/* force structure storage alignment */char__ss_pad2[_SS_PAD2SIZE];};

It's the same concept asstruct sockaddr, just longer. It also has some fun business in the middle to ensure that the whole thing gets nicely aligned, but you can ignore that.

Individual address families then have individual "subclasses", which are compatible with the above layouts so you can cast pointers between the different types. For IPv4 addresses, the corresponding address type isstruct sockaddr_in:

structsockaddr_in{__uint8_tsin_len;sa_family_tsin_family;in_port_tsin_port;structin_addrsin_addr;charsin_zero[8];};

Thesin_port field is the TCP or UDP port of the address, andsin_addr is the actual four-byte IPv4 address. Together, those make up a full IPv4 "address".

IPv6 is similar, but longer:

structsockaddr_in6{__uint8_tsin6_len;/* length of this struct(sa_family_t) */sa_family_tsin6_family;/* AF_INET6 (sa_family_t) */in_port_tsin6_port;/* Transport layer port # (in_port_t) */__uint32_tsin6_flowinfo;/* IP6 flow information */structin6_addrsin6_addr;/* IP6 address */__uint32_tsin6_scope_id;/* scope zone index */};

Thesin6_port field serves the same purpose as thesin_port field above, and thesin6_addr field is the 16-byte IPv6 address. Thesin6_flowinfo andsin6_scope_id fields are specialized fields you generally don't need to pay much attention to, and we'll skip over those.

Listening For Connections
Let's look at how to listen for incoming TCP connections. After creating the socket, you must then bind it to an IP address. This can be a specific IP address belonging to the computer you're on, or it can just be a special address meaning "listen on everything" which is usually what you want.

To bind to an address, you need a socket address. In this case, we'll use astruct sockaddr_in6 and create an IPv6 socket. This has the bonus of also accepting IPv4 connections, so one piece of code can accept both IPv4 and IPv6. We'll start off with the basic length and family information:

structsockaddr_in6addr={.sin6_len=sizeof(addr),.sin6_family=AF_INET6,

We'll hardcode the port number. Note that port numbers in socket address structs are always innetwork byte order, which is to say big-endian, so you need to use a byte-swapping function when putting a value in or getting a value out. The most natural functions to use here are the POSIX-levelhtons and friends, but any byte swapping function will do the job. Here we'll listen on port12345:

.sin6_port=htons(12345),

Finally, we need to specify that this is the special "any" address by using the constantin6addr_any for the address field:

.sin6_addr=in6addr_any};

Let's not forget to actually create the socket:

ints=socket(AF_INET6,SOCK_STREAM,0);

Now we're ready to bind it to the address. The call for this is named, naturally,bind. Thebind function takes three parameters: the socket to operate on, the address to bind to, and the length of that address. Here's the call:

bind(s,(void*)&addr,sizeof(addr));

The second parameter is cast tovoid * because the function takes astruct sockaddr * butaddr is astruct sockaddr_in6. Such is the peril of trying to provide a family of multiple semi-compatiblestructs.

You might be wondering why there's a length parameter, when the address itself also contains a length field. The POSIX standard doesn't actually require the length field, so some systems offer it and others don't. This means that any cross-platform code or interfaces (like the POSIX APIs themselves) can't assume the existence of the length field, and must pass it around separately.

With the socket bound, the next step is to tell the system to listen on it. This is done with, you guessed it, thelisten function. It takes two parameters: the socket to operate on, and the desired length of the queue used for listening. This queue length tells the system how many incoming connections you want it to sit on while trying to hand those connections off to your program. Unless you have a good reason to use something else, passing theSOMAXCONN gives you a safe, large value. Here's the call:

listen(s,SOMAXCONN);

The socket is now in the listening state and you can attempt to connect to it on port12345. The program must now accept incoming connections, which is done with theaccept function. This function takes three parameters: the socket to operate on, a place to store the address of the incoming connection, and the length of that storage. This allows you to find out where incoming connections are coming from, but they're not strictly necessary, so we'll leave them asNULL. The function returns a socket for the incoming connection:

intconn=accept(s,NULL,NULL);

You can then read data from this new socket:

intcount=read(conn,buf,sizeof(buf));printf("%.*s\n",count,buf);

When reading and writing data to a socket, youmust write your code to accept reading or writing less data than requested. Theread andwrite function calls return the number of bytes actually read or written. You can get away with ignoring this value in a lot of situations, but not so with socket programming. The amount of data read or written will frequently be less than what you requested when dealing with sockets, so you must write the code to buffer the data and loop in order to make multiple calls. For example, to write the above data back out, you'd want a loop like this:

intwriteCursor=0;intwriteCount;do{writeCount=write(conn,buf+writeCursor,count-writeCursor);writeCursor+=writeCount;}while(writeCursor<count);
Really, this is not quite correct. I'm trying to skip over error handling, but there is one error case that can't be ignored here. It is possible for aread orwrite call to return anEINTR error, which is a transient error that indicates that the system call was interrupted somehow. It doesn't indicate a failure, but rather just requires that you try the call again. Here's corrected code for that:
intwriteCursor=0;intwriteCount;do{writeCount=write(conn,buf+writeCursor,count-writeCursor);if(writeCount<0){if(errno!=EINTR){perror(write);break;}}else{writeCursor+=writeCount;}}while(writeCursor<count);

When you're done with the socket, justclose it:

close(conn);

If you want finer-grained control, you can use theshutdown function instead. This allows closing only one direction of the socket, which can be useful for certain protocols.

Making Connections
To make a connection, you first need an address to connect to. There are about sixteen billion different ways to obtain an address, from hardcoding it to writing code to parse IP addresses to asking the system to translate a human-readable string into a connectable address.

Fortunately for us, the APIs for getting the system to do the work for us are relatively easy to use. The modern call isgetaddrinfo. It's a capable API with a lot of options, but basic usage is straightforward.

First, we need a hostname. You'd probably get this from a UI or something, but in this case we'll just hardcode it:

constchar*name="mikeash.com";

We also need a port number. We could smash this into the addressstruct ourselves later, but it's easier to hand it togetaddrinfo and let it worry about that part:

intport=80;

getaddrinfo actually wants the port number to be in the form of a string. This makes it really convenient if your port number originates as a string, but in this case it means we need to do a small amount of extra work. I'll transform this integer port into a string usingasprintf:

char*portString;asprintf(&portString,"%d",port);

getaddrinfo takes a set of "hints", which allow control over what kind of results it will provide. There are many options, but the only one we care about here is the socket protocol. The call can provide results with different socket protocols, such as TCP or UDP, and we want to ensure we only look for TCP. If we don't do that, the call will return two results for each address it finds, one for each protocol. To specify TCP, we just specifyIPPROTO_TCP in theai_protocol field:

structaddrinfohints={.ai_protocol=IPPROTO_TCP};

Everything is now ready to make the call togetaddrinfo:

structaddrinfo*info;getaddrinfo(name,portString,&hints,&info);

One hostname can potentially have many addresses. Thestruct addrinfo returned fromgetaddrinfo is actually a linked list, which can be walked in order to enumerate all of the results:

for(structaddrinfo*cursor=info;cursor;cursor=cursor->ai_next){

Let's go ahead and print them all out.struct addrinfo contains anai_addr field which points to astruct sockaddr. We can convert this into a human-readable string usinggetnameinfo like so:

charaddrStr[NI_MAXHOST];getnameinfo(cursor->ai_addr,cursor->ai_addrlen,addrStr,sizeof(addrStr),NULL,0,NI_NUMERICHOST));

We'll print it out along with some of the other pertinent fields:

printf("flags=%x family=%d type=%d protocol=%d address=%s\n",cursor->ai_flags,cursor->ai_family,cursor->ai_socktype,cursor->ai_protocol,addrStr);}

Forgoogle.com this produces a nice list of addresses:

flags=0family=2type=1protocol=6address=74.125.228.228flags=0family=2type=1protocol=6address=74.125.228.224flags=0family=2type=1protocol=6address=74.125.228.232flags=0family=2type=1protocol=6address=74.125.228.227flags=0family=2type=1protocol=6address=74.125.228.230flags=0family=2type=1protocol=6address=74.125.228.233flags=0family=2type=1protocol=6address=74.125.228.231flags=0family=2type=1protocol=6address=74.125.228.229flags=0family=2type=1protocol=6address=74.125.228.238flags=0family=2type=1protocol=6address=74.125.228.226flags=0family=2type=1protocol=6address=74.125.228.225flags=0family=30type=1protocol=6address=2607:f8b0:4004:803::1009

We're ready to create a socket now. We'll just grab the first value in the list for this part. In real code, you'd want to iterate over the list and try additional entries if one failed, and possibly try multiple entries simultaneously for better speed. Theai_family,ai_socktype, andai_protocol fields provide everything we need to create the socket:

ints=socket(info->ai_family,info->ai_socktype,info->ai_protocol);

Now we need to make it connect to the address. This is done with the aptly-namedconnect function, which takes the socket, the destination address, and its length:

connect(s,info->ai_addr,info->ai_addrlen);

Upon successful completion of this call, we have a connected socket to the target address. We can now read and write using this socket. Before we do that, since we're all done with the address data at this point, we'll go ahead and free it:

freeaddrinfo(info);

Let's not forget the port string:

free(portString);

Since we're connecting to port 80, we can write an HTTP request to this socket:

constchar*toWrite="GET /\r\n\r\n";

Since sockets are file descriptors,write works fine on them. As always, be sure to use a loop:

while(*toWrite){intwritten=write(s,toWrite,strlen(toWrite));if(writeCount<0){if(errno!=EINTR){perror(write);break;}}else{toWrite+=written;}}

Now we can read the response, also using a loop:

charbuf[1024];intcount;do{count=read(s,buf,sizeof(buf)))>0);if(count<0){if(errno!=EINTR){perror("read");break;}}else{printf("%.*s",count,buf);}}while(count>0);

Whenread returns0, that indicates that the server has closed the connection and we can then close our end of things:

close(s);

Conclusion
The POSIX sockets API is a bit old and crusty, but it's ultimately not too bad. You should use higher-level APIs whenever it's reasonable, but it's good to understand what the low-level API does and how to use it even if you don't actually use it too often.

That's it for today. Come back next time for more amusing adventures. Friday Q&A is driven by reader suggestions, so if you have a suggestion for a topic to cover in the meantime, pleasesend it in!

Did you enjoy this article? I'm selling whole books full of them! Volumes II and III are now out! They're available as ePub, PDF, print, and on iBooks and Kindle.Click here for more information.

Comments:

Jesperat2014-03-14 14:17:26:
What does "XSI" in the code comments mean?
Scott Talbotat2014-03-14 14:56:41:
Wouldn't you want to use "htons", not "ntohs" for that port specification?
Considering that you're translating from Host to Network byte ordering?
Danielat2014-03-14 15:12:54:
sockets first parameter (domain) is PF_INET6, not AF_INET6. It chooses the ProtocolFamily, not the AddressFamily.

You don't have to fill the sin6_len field. It's better to leave it alone, just in case you want to port your code to a different POSIX platform one day.

getaddrinfo's port parameter is called "servname" for a reason: It's not (just) the port but the service name. You can say "http". getaddrinfo will look up the default port for this service. (The list it uses is in /etc/services)

Maybe it's good to know that you always should iterate over the results of getaddrinfo until you succeed to connect: In your example, the first result is IPv4 but your machine might run IPv6 only.
Steve Madsenat2014-03-14 15:35:56:
Jesper: my best guess is that "XSI" refers to the X/Open System Interface, i.e., POSIX. That comment appears in several header files on other platforms and Mike likely copied it straight out of the header.

A general comment, though: there are many reasons to avoid dropping to POSIX for client-side networking. Apple mentions many of the advantages to sticking with the higher level APIs in the '11 and '12 WWDC videos. Two big ones: automatic tuning of socket buffer sizes for performance and simultaneous connection attempts over both IPv4 and v6 when a destination host name resolves to both.

Unfortunately, if you're writing server-side code, the higher level APIs are far less useful (NS/CFStream can't listen).
Xavier Morelat2014-03-14 20:45:11:
ntohs converts from network byte order to host byte order, if the port must be in network byte order don't we wanthtons instead, to convert from our literal in host byte order to network BO? (since it's a 16 bit value it's just going to swap both bytes and work either way, but…)
Alex Johnsonat2014-03-15 20:21:06:
I think there's a mistake in the sample socket-writing code. I think you meant to check `writeCursor`, rather then `writeCount`, as in `while (writeCursor < count)`. Really, though, I think it should be `while (writeCount != -1)`, since otherwise it will loop forever when the write fails (right?).
Adnakoat2014-03-18 05:54:52:
+1 for PF_INET6

http://tools.ietf.org/html/rfc2133
...
3.1.
...
The PF_INET6 is used in the first argument to the socket() function
   to indicate that an IPv6 socket is being created.
...
Jakob Eggerat2014-03-18 08:05:53:
Daniel: Are you sure you can leave out the sin6_len field? I had a problem in one of my apps where connect() would fail on some version of Mac OS X. I thought I fixed it by setting the length field in the sockaddr struct.
mikeashat2014-03-19 01:43:32:
Thanks for all the comments. I've fixed up a couple of problems with the article, including a failure to handle the importantEINTR error which is not at all optional, and that sillyntohs mixup.

Regarding the AF/PF debate, I'm afraid it's completely obsolete. You'll find thatPF_INET and friends no longer exist in the latest POSIX spec. The distinction has never mattered in practice, and now it seems it doesn't even matter in theory.
tesat2014-04-12 21:55:56:
Great write up Mike. One small issue, you forgot to allocate memory for portString, but did free it later.
Jose Vazquezat2014-04-15 15:35:22:
tes: He used asprintf() to create the string. Check out the docs and you'll notice that unlike sprint(), asprintf() will allocate memory for the string. The docs also say it must later be free()'d, which he did.
Nicat2015-03-19 02:32:43:
After developing iOS apps for two years, I stared to want to know this lower-API is written and how to use those in iOS programming. And Thank God, I came across your blog. Really appreciate it.

But, how to port these code to Xcode ? Can we use these code in iOS programming ? Much appreciate if there is example for porting to Xcode and iOS

Thanks

Comments RSS feed for this page

Add your thoughts, post a comment:

Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.

Name:
The Answer to the Ultimate Question of Life, the Universe, and Everything?
Comment:
Formatting:<i> <b> <blockquote> <code>.
NOTE: Due to an increase in spam, URLs are forbidden! Please provide search terms or fragment your URLs so they don't look like URLs.
Code syntax highlighting thanks toPygments.
Hosted atDigitalOcean.

[8]ページ先頭

©2009-2025 Movatter.jp