BSD SOCKETS

来源:互联网 发布:最新mac系统使用教程 编辑:程序博客网 时间:2024/05/16 06:22

BSD SOCKETS

What became the BSD socket API was first implemented in the late 1980s by researchers at the University of California at Berkley. It was eventually standardized as the Portable Operating System Interface (POSIX) sockets API by the Institute of Electrical and Electronics Engineers (IEEE) as the standard for UNIX and all UNIX-like operating systems. Its popularity and relative ease-of-use has also inspired a similar implementation for the Winsock API for Microsoft Windows. At some layer, BSD sockets carry almost all Internet traffic and give application programmers absolute control over any communication to a remote device or server. A single socket is a one-way connection between two endpoints; thus they are typically created in pairs: one for reading and one for writing. Like almost all other resources on UNIX systems, sockets are represented as files and are assigned a file descriptor when created. The six most common socket API calls are summarized in Table 8-1.

Table 8-1: BSD SOCKET API CALLS

API CALLDESCRIPTIONint socket(int addressFamily, int type, int protocol)Creates and initializes a new socket. Returns a file descriptor number on success and -1 on failureint bind(int socketFileDescriptor, sockaddr *addressToBind, int addressStructLength)Assigns the socket to the address and port specified in the addressToBindstructint accept(int socketFileDescriptor, sockaddr *clientAddress, int clientAddressStructLength)Accepts a connection request and stores the client’s address intoclientAddressint connect(int socketFileDescriptor, sockaddr *serverAddress, int serverAddressLength)Connects to the server specified inserverAddresshostent* gethostbyname(char *hostname)Attempts to use DNS to find an IP address corresponding to the provided hostnameint send(int socketFileDescriptor, char *buffer, int bufferLength, int flags)Sends up to bufferLength bytes frombuffer across the socketint receive(int socketFileDescriptor, char *buffer, int bufferLength, int flags)Reads up to bufferLength bytes from the socket into bufferint sendto(int socketFileDescriptor, char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength)Sends up to bufferLength bytes frombuffer to destinationAddressint recvfrom(int socketFileDescriptor, char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength)Reads up to bufferLength bytes from the socket into buffer and stores the sender’s address into fromAddress

BSD sockets are implemented strictly in C and can be used unmodified in Objective-C code. This is convenient if you would like to reuse an existing networking library, use a port code from another platform with little hassle, or if you have previous socket experience and do not want to spend resources learning one of Apple’s higher-level frameworks. Apple recommends against this practice, however, because raw sockets don’t have access to built-in networking features of the OS like the system wide VPN. Even worse, initiating a socket connection won’t automatically turn on the device’s Wi-Fi or cellular radios. The radios are intelligently turned off to save battery power, and any communication attempts will fail until some other networking process activates the radio. CFNetwork’s wrapper around BSD sockets can activate the device’s radio; thus it is recommended over BSD sockets in almost every scenario.

To create a socket, call socket(int addressFamily, int type, int protocol) with the desired networking domain, socket type, and protocol enumeration values from socket.h. Typically, the addressFamily is either IPv4 (AF_INET) or IPv6 (AF_INET6) for traffic originating from an iOS app; however, you can also open a socket to a local file. The socket type is commonly set to stream (SOCK_STREAM) or datagram (SOCK_DGRAM). These two values are important because socket() is frequently called with a protocol value of 0, which indicates that the system can use the domain and type values to automatically choose the appropriate protocol. For stream sockets, the automatic value is Transmission Control Protocol (IPPROTO_TCP) and for datagram sockets it is User Datagram Protocol (IPPROTO_UDP). The semantics of these two protocols are discussed in more detail in Chapter 12, “Device-to-Device Communication with GameKit.” If the socket is successfully created, the returned value is the number of the new file descriptor; however, if the call fails for any reason, the return value will be -1. At this point, no communication has occurred yet, and the socket has not been designated as an input or output socket (this won’t happen until the socket is used for the first time). Clients are now ready to begin connecting to a server; however, a server requires one or more calls before it is ready to communicate.

Configuring a Socket Server

The BSD socket server must associate the socket with a unique address by calling bind(int socketFileDescriptor, sockaddr *addressToBind, int addressStructLength). This takes the socket and assigns, or binds, it to a specific address and port. It returns 0 for a successful bind and -1 otherwise. After the socket is bound, the next step depends on the type of connection you specified in the socket() call, either UDP or TCP:

  • For UDP sockets, you are ready to start transmitting data to the world because UDP is a connection-less protocol and doesn’t require someone listening on the other end.
  • TCP sockets are connection-oriented and require a participant on the other end of the socket. To establish a connection for TCP, you must call listen(int socketFileDescriptorint backlogSize) to set up the data structure for the backlog queue.

The socket passed as the first argument becomes a read-only socket and can’t be used to send messages. The backlogSize indicates how many pending connections can be queued up while waiting to be acknowledged by your server code. When listening, the server waits for an incoming connection request and calls accept(int socketFileDescriptor, sockaddr *clientAddress, int clientAddressStructLength) to accept the request. This removes the pending request from the backlog queue and populates the clientAddress struct with the client’s addressing information, most importantly its IP address and port. After the pending request has been accepted, the server is ready to receive messages from a client.

Connecting as a Socket Client

A client’s first action depends on the protocol in use by the socket. For TCP sockets, the client must first negotiate the connection to a server withconnect(int socketFileDescriptor, sockaddr *serverAddress, int serverAddressLength)The call blocks while the TCP handshake occurs and then returns 0 on success or -1 on failure. For UDP sockets, connect() is optional; however, calling it sets the default address for the socket for all UDP traffic. This makes sending and receiving UDP datagrams convenient. If the device connects to a hostname instead of an IP address, it is probably not clear how to proceed because sockaddr struct contains only an IP address. TheDomain Name System (DNS) was created to solve this problem of converting a hostname to an IP address. The hostent* gethostbyname(char *hostname)function makes a blocking DNS query for the specified hostname. The hostentstruct contains a list of IP addresses for the host in a format directly compatible with the sockaddr struct used through the sockets API. If hostname contains an IP address in dot notation, the call simply populates the returned hostent’s first result with the given IP address and returns immediately. This behavior makes it easy for the user to provide either an IP address or hostname without needing to branch the connection logic. If a DNS entry cannot be found for the supplied hostname, gethostbyname() returns NULL.

Once the socket is connected, the device can send or receive messages across it. There are two pairs of socket API calls to use, and the correct pair depends on the type of socket in use. TCP sockets use the int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags) andint receive(int socketFileDescriptor, char *buffer, int bufferLength, int flags) pair. When sending, the socket described bysocketFileDescriptor transmits buffer’s bytes between 0 and bufferLength. If successful, it returns the number of bytes successfully sent and -1 for any failure. When receiving, buffer is populated with a copy of the firstbufferLength bytes read from the socket. Similarly to send()receive() also returns the number of bytes successfully read and -1 for any failure. UDP sockets that previously used connect() to set the default address can also usesend() and receive() in the same manner as TCP sockets. Otherwise, UDP sockets must use the second pair of API calls used specifically for connectionless protocols.

UDP sockets can send to multiple addresses using the same socket connection with the int sendto(int socketFileDescriptor, char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength) API call. This call behaves similarly to send()except it has additional arguments for the destination address. Because UDP doesn’t make any guarantee about message delivery, it can immediately use the socket to send to another address via another sendto() call. The corresponding connectionless receiving call is int recvfrom(int socketFileDescriptor, char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength) and behaves similarly tosendto(). Note one important difference: The last argument is a pointer to an integer that will be populated with the final length of the fromAddress struct. Because UDP sockets are not usually connected to a single endpoint, the code receiving a datagram needs to know where it came from, and recvfrom()populates fromAddress with that information.

Now that all the pieces are in place to connect to other devices and send or receive data, the following example ties everything together. The example app connects to a monitoring server in a warehouse that controls its alarm and climate control systems. The example app must connect using a low-level networking framework because the warehouse server reports its results over the telnet protocol, which isn’t directly supported by the higher-level objects such as NSURLConnection. Three separate networking controllers each load the same telnet results using a different low-level framework and display the fetched results to the user. The warehouse server responds with a string formatted according to the following snippet of code:

84,60,+67,1,1,0,0,0,1{room temperature},{outlet temperature},{coil temperature},{compressor status},{air switch status},{auxiliary heat status},{front door status},{system status},{alarm status}

Each controller fetches the data in a background thread, as shown in Listing 8-1, to prevent the user interface from blocking during the network communication.

LISTING 8-1: Fetching Results in a Background Thread (LLNNetworkingController.m)
- (void)start {    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"telnet://%@:%i",        self.urlString, self.portNumber]];     NSThread *t = [[NSThread alloc] initWithTarget:self                                          selector:@selector(loadCurrentStatus:)                                            object:url];     [t start];}

The networking controller reports its results to the user interface through two delegate messages: networkingResultsDidLoad: takes the networking results as an argument, and networkingResultsDidFail: takes a user-readable message that indicates what went wrong. The complete socket implementation to load the warehouse results is shown in Listing 8-2.

LISTING 8-2: Loading Results with BSD Sockets (LLNBSDSocketController.m)
- (void)loadCurrentStatus:(NSURL*)url {    if ([self.delegate respondsToSelector:@selector(networkingResultsDidStart)]) {        [self.delegate networkingResultsDidStart];    }        // create a new Internet stream socket    socketFileDescriptor = socket(AF_INET, SOCK_STREAM, 0);        if (socketFileDescriptor == -1) {        if ([self.delegate respondsToSelector:                @selector(networkingResultsDidFail:)]) {             [self.delegate networkingResultsDidFail:                @"Unable to allocate networking resources."];        }                return;    }            // convert the hostname to an IP address    struct hostent *remoteHostEnt = gethostbyname([[url host] UTF8String]);        if (remoteHostEnt == NULL) {        if ([self.delegate respondsToSelector:                @selector(networkingResultsDidFail:)]) {             [self.delegate networkingResultsDidFail:                @"Unable to resolve the hostname of the warehouse server."];        }                return;    }        struct in_addr *remoteInAddr = (struct in_addr *)remoteHostEnt->h_addr_list[0];        // set the socket parameters to open that IP address    struct sockaddr_in socketParameters;    socketParameters.sin_family = AF_INET;    socketParameters.sin_addr = *remoteInAddr;    socketParameters.sin_port = htons([[url port] intValue]);        // connect the socket; a return code of -1 indicates an error    if (connect(socketFileDescriptor, (struct sockaddr *) &socketParameters,             sizeof(socketParameters)) == -1) {         NSLog(@"Failed to connect to %@", url);                if ([self.delegate respondsToSelector:                @selector(networkingResultsDidFail:)]) {             [self.delegate networkingResultsDidFail:                @"Unable to connect to the warehouse server."];        }                return;    }        NSLog(@"Successfully connected to %@", url);     NSMutableData *data = [[NSMutableData alloc] init];    BOOL waitingForData = YES;        // continually receive data until you reach the end of the data    while (waitingForData){        const char *buffer[1024];        int length = sizeof(buffer);                // read a buffer's amount of data from the socket; the number         // of bytes read is returned        int result = recv(socketFileDescriptor, &buffer, length, 0);                // if you got data, append it to the buffer and keep looping        if (result > 0){            [data appendBytes:buffer length:result];                    // if you didn't get any data, stop the receive loop        } else {            waitingForData = NO;        }    }        // close the stream since you are done reading    close(socketFileDescriptor);     NSString *resultsString = [[NSString alloc] initWithData:data                                                     encoding:NSUTF8StringEncoding];    NSLog(@"Received string: '%@'", resultsString);        LLNNetworkingResult *result = [self parseResultString:resultsString];        if (result != nil) {        if ([self.delegate respondsToSelector:                @selector(networkingResultsDidLoad:)]) {             [self.delegate networkingResultsDidLoad:result];        }            } else {        if ([self.delegate respondsToSelector:                @selector(networkingResultsDidFail:)]) {             [self.delegate networkingResultsDidFail:                @"Unable to parse the response from the warehouse server."];        }    }}

The sockets controller begins by creating a new Internet stream socket withsocket(). It then takes the server’s hostname and uses gethostbyname() to get its IP address. If both of these calls are successful, the app is ready to populate the sockaddr_in struct with the hostname and port, and then use it to connect to the server. When setting the port, the integer value is converted to network byte order with htons() to ensure it can be read properly by both big endian and little endian systems. In the connect() call, sockaddr_in is cast to a sockaddr, and this is possible because both structs have the same layout when sockaddr_in.sin_family is AF_INETWhen the socket is connected, the app is ready to read any available data into an NSMutableData for later processing. It reads in 1,024-byte chunks until a read call indicates that no more data is available. After all the data is downloaded, it is converted to a string, parsed into each individual piece of environment data, and given to the UI for display to the user. Cleaning up a socket is as easy as calling close() on the socket file descriptor.

0 0
原创粉丝点击