CSE 422S: Studio 18

Linux Sockets

Sockets are a method of IPC that allow data to be exchanged between applications, either on the same host (computer) or on different hosts connected by a network.

—Michael Kerrisk, The Linux Programming Interface, Chapter 56, pp. 1149.

Sockets provide efficient ways to do file-like input and output between processes on the same host, or on different hosts. Their biggest benefit is that they allow bi-directional communication as well as the ability to treat the endpoints as formatted input and output streams, just like you would read and write from files, or from streams like standard input and output.

In this studio, you will:

  1. Use sockets to send data between processes locally within a machine
  2. Use sockets to send data between processes running on different machines

Please complete the required exercises below. We encourage you to please work in groups of 2 or 3 people on each studio (and the groups are allowed to change from studio to studio) though if you would prefer to complete any studio by yourself that is allowed.

As you work through these exercises, please record your answers, and when finished upload them along with the relevant source code to the appropriate spot on Canvas. If you work in a group with other people, only one of you should please upload the answers (and any other files requested) for the studio, and if you need to resubmit (e.g., if a studio were to be marked incomplete when we grade it) the same person who submitted the studio originally should please do that.

Make sure that the name of each person who worked on these exercises is listed in the first answer, and make sure you number each of your responses so it is easy to match your responses with each exercise.

Required Exercises

  1. As the answer to the first exercise, list the names of the people who worked together on this studio.

  2. You will first use sockets to establish a local connection between two processes on your Raspberry Pi. We'll write two programs, a server and a client. First we will write the server, whose role is to create the communications channel and to listen for incoming connections. A good example of how to construct a basic server can be found in man 2 bind under the section labeled EXAMPLE.

    Your server should perform the following actions:

    1. First, create a socket with the socket() system call. To create a local connection use the domain AF_LOCAL (or equivalently AF_UNIX), the connection type SOCK_STREAM, and protocol zero.
    2. Second, create the communications channel with the bind() system call. This associates your socket from step one with a machine-visible address. In the case of AF_LOCAL, this address is a path in the file system, and your local server program should use the first argument passed to it as a string containing that path. Directions on how to specify a local address in this way (and which header files you'll need in order to make that work) can be found at man 7 unix, and there is a good example showing how to do this on page 1166 of the LPI (Kerrisk) text book.
    3. Now, having created the channel, your program needs to declare that it is going to listen for incoming connections with the listen() system call. Sockets only allow one process to connect at a time, so the second parameter determines how many connection requests can wait (queue up) before subsequent ones are rejected.
    4. Use the accept() system call to accept a connection over the socket interface, passing in NULL and NULL for the second and third arguments. If no connection is immediately available then this system call will, by default, cause your program to block until another program attempts to connect. When your program successfully returns from the call to accept() the new communication socket connection has been established, and the return value from the call is a file descriptor for the new socket.
    5. Your server program then should repeatedly read unsigned integers from the new socket, by instantiating a stream interface for it via the fdopen function and then using fscanf(). When creating the stream, be sure to use the socket descriptor that was returned by the call to accept() (the communication endpoint), not the one created by the initial call to socket() (the connection establishment endpoint). As long as the communication endpoint socket remains open, the server should print out all unsigned integers that it receives, to its standard output stream.
    6. Once the client disconnects, use the unlink() system call in order to destroy the original local socket (connection establishment endpoint) file before the program returns.

    Note: Many things can go wrong with socket-based communication. As usual, you should always check function return codes to detect errors. Recall that most functions return error codes that allow you to print a descriptive error statement with a line of code such as: printf("Program error, reason: %s\n", strerror(errno)). This will require including the headers string.h and errno.h.

    Now you will write a client program that should do the following:

    1. Create a communications endpoint with a call to the socket() system call.
    2. Establish a connection with the connect() system call. This requires using the same local socket address as was used previously in your local server program's call to bind().
    3. At this point, barring any errors, the communcations channel is ready to be used from the client side. Your client program should send some unsigned integers to the server program and print them to its standard output stream in order to validate the functionality; the client program should then close the socket and end.

    Compile your client and server programs, and run them in separate terminal windows on your Raspberry Pi. Copy and paste the client and server programs' output as the answer to this exercise.

  3. Refactor your server so that (in addition to the loop it uses to read repeatedly from the same socket as long as it's open) it uses an outer event loop, so that it will stay alive through multiple connection attempts. This requires that your server make multiple calls to accept() (but not to listen() or bind()). Your server program should also print a message announcing each time a new connection is established and should then print out the unsigned integer values it reads from the socket each time, to the standard output stream.

    Rather than having your server stay alive forever, now modify your server program so that it terminates when it recieves a special unsigned integer value (e.g., the http status code 418). Modify your client so that it can accept zero or one command line arguments, and if it is given the command line argument "quit" it sends that special code to the server after it has sent its other unsigned integers (thus terminating the server).

    Compile your refactored server code and run it in its own terminal window. In another window, run your client program several times along with that server to validate that it can stay alive and keep accepting new connections until it is told to quit. Copy and paste the server output as the answer to this exercise.

  4. The primary use of sockets is to manage networked connections between remote computers. This exercise will have your client program connect from your Raspberry Pi, to your server program that is running on the shell.cec.wustl.edu host.

    Note that for this exercise, your server program will run directly on shell.cec.wustl.edu. You should not qlogin into a Linux Lab node.

    Make copies of your client and server programs, and modify the new copies to use an internet connection rather than a local connection. Choose a port number between 30000 and 40000 that both of your programs will use wherever a port number is needed in the instructions below. Run the command

    ping shell.cec.wustl.edu

    (on the Raspberry Pi you may need to preface that command with sudo)

    from a terminal window to obtain the numeric IP address of the shell.cec.wustl.edu machine, which your client program should use.

    1. Include the files netinet/ip.h and arpa/inet.h in both programs.
    2. In both programs, modify your calls to socket() (and bind() and connect(), respectively) to specify an internet connection type with AF_INET (instead of AF_LOCAL or AF_UNIX). Also remove any artifacts of the local domain socket from both programs, including removing the call to unlink() from the server code.
    3. In both programs, declare a struct of type sockaddr_in (instead of sockaddr_un) to specify the internet address. This struct is documented at man 7 ip. In both programs, set the sin_family field to AF_INET, and the sin_port field to htons(port_num), where htons() is a function that translates between the numeric representations used on your computer and in the network, and port_num is the port number that both your server and client programs are using. In the server program assign the sin_addr.s_addr field the value INADDR_ANY, and in the client program use the function inet_aton() to translate a c-style string containing the target IP address and store it in the sin_addr field of the sockaddr_in struct.

    Compile and run your server on shell.cec.wustl.edu. Compile your client on your Raspberry Pi and run it there twice, once without telling the server to quit and the second time telling the server to quit. As the answer to this exercise, copy and paste the output from your client and server terminal windows. If you cannot connect for whatever reason, give a descriptive error message using strerror() and errno.

  5. Modify your client and server programs from the previous exercise so that when it starts up each of them prints out the fully qualified name of the host it is running on (e.g., shell.cec.wustl.edu rather than just shell). After its socket is connected, the client program should also print out that socket's IP address and port. The server program should also print out the IP address and port of the socket on which it is listening, and should also print out the IP address and port of each additional socket that it accepts. As the answer to this exercise please copy and paste all of that output from your client and server terminal windows.

    Using the gethostname(), getnameinfo(), and/or getaddrinfo() functions may be helpful for this exercise.

  6. Modify your client and server programs from the previous exercise so that each of them treats the unsigned integers as binary data, (1) using htons or htonl to convert each of them to network byte order before using the write syscall to send them over the socket, and (2) using ntohs or ntohl to convert each of them back to host byte order after using the read syscall to get them from the socket. Please re-test your programs to make sure that they are working correctly, and as the answer to this exercise, please explain briefly which of those conversion functions you used in each of the programs and why.

  7. Modify your client and server programs from the previous exercise so that each of them tries to send and receive reasonably large contiguous arrays of unsigned integers within a single write or read call. If you have not done so already, modify those programs so that they check for and correctly handle short writes and short reads. Please re-test your programs to make sure that they are working correctly, and as the answer to this exercise, please explain briefly how your programs detect, and how they handle, short writes and short reads.

    Please Note: If your Raspberry Pi is not on the school's network, you will need to first establish a remote connection, either with a VPN connection or through an SSH tunnel.

    Installing the VPN Client

    To install the VPN client on your Raspberry Pi, first click the raspberry icon on the top left corner of your desktop, select Preferences, and then select Add/Remove Software. In the menu that pops up, search for the keyword "openconnect". Then click on the following 3 items:

    After you have selected all 3 options, click OK to install the packages (you will be prompted to enter the new password you created on your Pi above).

    These instructions have been updated to use the new WashU 2FA (two-factor authentication) based VPN. If you have any problems with enrolling in or using WashU 2FA please call the WashU IT ServiceDesk at (314) 933-3333, or email ithelp@wustl.edu.

    Once you have installed the VPN, and have enrolled in WashU 2FA and have chosen which authentication method to use, please open up a terminal window on your Raspberry Pi, and type:

    sudo openconnect danforthvpn.wustl.edu

    At the Username: prompt please enter you WUSTL key ID. After that you should get a Password: prompt, where you should enter your WUSTL key password. You will then get a second prompt that also says Password: which is where you should enter one of the following keywords (depending on which authentication method(s) you set up when you enrolled in WashU 2FA):

    push -- enter this if you have set up DUO mobile notification (be aware that if you have more than one device enrolled, you may receive the push on only one device)

    phone -- enter this if you want to authenticate via a phone call to the number you have registered with the WashU 2FA system (this may only work if you only have registered a single number there).

    (note that other methods like entering sms also may work but involve other details that we have not tested out - please contact the WashU IT ServiceDesk at (314) 933-3333 or ithelp@wustl.edu, or contact Engineering IT for help using those other methods.)

    Note: you will need to go through this process to start the VPN every time you reboot your Raspberry Pi and want to access the linuxlab machines directly from it.

    Establishing an SSH Tunnel

    An SSH tunnel allows you to forward traffic on a specific port to a remote server via SSH.

    To establish a tunnel, simply run the following command on your Raspberry Pi:

    ssh -f -N your-username@shell.cec.wustl.edu -L port-number:shell.cec.wustl.edu:port-number

    You must replace your-username with your WUSTL Key username, and port-number with the port you selected to establish the socket connection.

    For example, your command might look like: ssh -f -N msudvarg@shell.cec.wustl.edu -L 31522:shell.cec.wustl.edu:31522

    After issuing the command, you will be prompted for your WUSTL Key password. Alternatively, if you are already set up to log in with an SSH key, you may replace your-username@shell.cec.wustl.edu with the server nickname, as detailed on this page.

    If you use the SSH Tunnel approach, your socket code should be configured to instead connect to the local loopback IP address (i.e. instead of the IP address you obtained for shell.cec.wustl.edu.

Things to turn in: