Feedback Search Top Backward
EDM/2

Plugging into OS/2 Socket Programming - Part 1/3

Written by Edward D. Boykin

 
Part1 Part2 Part3

Introduction

Heya! I would like to welcome you folks to a new series of EDM/2 articles on Client/Server programming with OS/2 Warp. Client/Server programming encompasses a whole slew of different forms; so, for now, this series will focus entirely on the peer-to-peer LAN protocol referred to as "sockets".

I am guessing that there will be three or four articles in this series but there might be more or less depending on how well I feel I am covering the topics. This is my first shot at writing anything even remotely like this so if I stumble some please bear with me. Thanks!

If anybody notices some errors or better ways of doing some of the things I am doing please feel free to e-mail me. The same goes if you have any questions on any of the subjects discussed here.

I must also make another apology. The code I use here is based on the IBM TCP/IP Development Toolkit which is a commercial product. I am therefore unable to distribute the header files needed to compile the source code. I have included copies of the executables so that you may see the results of the code. The executables are compiled with full debugging on so you can step through it if you have the IPMD debugger. I would make the code portable between EMX and C- Set++ but I don't have the time.

Path of Study

Over the next few months I will cover everything necessary for you to get started in socket programming. The articles will start with a very simple client and server program which I will then develop into a basic message system allowing multiple client connections to a single server. It will allow the people on the client end to connect to the server and chat with anyone else connected to the same server in near real-time.

However, we have to walk before we can run. This month I will start with an introduction to the basic functions in the socket API. These are the minimal functions that are necessary to create the most basic client and server programs. I have included just such a set of programs and encourage you to examine them while you read through this article.

What is a Socket?

History

Back in 1981, sockets were first introduced as the UNIX BSD 4.2 generic interface that would provide point to point communications for TCP/IP networks. After a couple of revisions and some stiff competition from other similar protocols, the socket API has become a very prevalent peer to peer protocol. This is good because it means my articles are not wasted electrons.

Anyway, since then support for sockets has migrated to just about every operating environment including MS-DOS, OS/2, UNIX, MacOS etc. In the BSD UNIX system, sockets are integrated in the system kernel while in the non-BSD systems, like OS/2, the socket services are provided in the form of libraries. This wide range of availability makes sockets one of the best portable network communication protocols.

Description

A socket is a peer to peer communications construct. This construct basically consists of a name, or descriptor, and a network address. There are a few different protocol families in which a socket can operate. These include AF_INET, AF_UNIX, AF_PUP and AF_APPLETALK. OS/2 currently only supports AF_INET. This family designation is mainly used to define the addressing scheme which the socket will use.

There are three types of sockets which OS/2 can use. These are stream, datagram, and raw sockets. Stream sockets interface with TCP, datagram sockets interface with UDP, and raw sockets interface with ICMP and IP. The most commonly used socket type is the stream socket and this is what we will use in our sample programs.

The stream socket is the most commonly used because of its reliability. Packets sent over a stream socket are guaranteed to be sent without error or duplication and they will be received in the same order they are sent in. There are no boundaries imposed on the data. Think of the data as a simple stream of bytes flowing from one computer to another. The only real bummer with stream sockets is that they are slower and require some extra programming overhead.

NOTE: Because there are no data boundaries, TCP does not know where a message ends or begins. This means that if you send two packets to someplace you must have some way of letting they receiving end know how to delimit the two different packets. Don't worry; it's a piece of cake.

TCP/IP Internet Addressing

The basic socket address consists of an internet address and a port address. The internet address is a 32 bit number that many of us are used to seeing as a series of four three digit numbers that are separated by decimals. This number is unique for every network interface within a domain. For instance, I have two internet addresses on my computer at home. One of the addresses is for my PPP connection and the other is for my Ethernet card.

A port address is a 16 bit integer representing where the entry point for a particular service is on a server machine. If you have TCP/IP installed, you can look in the \TCPIP\ETC\SERVICES file and see a long list of the most commonly used ports. These are reserved and you should not set up any servers which use the ports for anything other than what the SERVICES file specifies.

The Socket API

The complete socket API consists of 58 different functions which can be broken down into 21 core functions and 37 utility functions. Since there are so many I will only explain 10 of the core functions and then only the utility functions I need for this months programs. As the series continues, I will use more functions and I'll be sure to explain them at that time.

Socket Setup Functions

These are the functions which are used by the client and server to set up the sockets on their respective ends.


int sock_init()
Figure 1) sock_init() function

The sock_init() call is used to initialize the socket data structures and check to see if INET.SYS is running. Every program which uses sockets must use this call before doing any socket related activities. The return values for this call are: 0 - Failure 1 - Success


int socket
  (
  int  domain,   // This is the address type. It must  always be AF INET
  int  type,     // This is the  type  of  socket created. SOCK_STREAM,
                 // SOCK_DGRAM, and SOCK_RAW are the possible choices
  int  protocol  // The protocol requested. Most commonly 0. Others are
                 // available.
  )
figure 2) socket() function

The purpose of the socket() function is to create a socket and return it descriptor. It will return a non-negative integer if the function succeeds and will return -1 if an error occurs.


int bind
   (
   int  s,                // This is the socket to which an address is
                          // bound
   struct sockaddr *name, // Pointer to the sockaddr structure containing
                          // the name to bind to socket s
   int  namelen           // Size of the buffer which  name points to in
                          // bytes
   )
Figure 3) bind() function

bind() is used to bind a unique port number to a socket. This is how a server uses a specific port for it's service. The system can assign a port for you automatically but there is no way for clients wanting to connect to your server to know what that port is. bind() will return 0 on success and -1 on failure.

A Brief Aside.....The sockaddr structure

The sockaddr structure, defined in <sys/socket.h>, is what it used to define a socket address. It is defined as follows:


struct sockaddr
   {
   ushort  sa_family,   //The address family which is always
                        //AF_INET for OS/2
   char    sa_data[14], // 14 bytes of direct access data which
                        //is different for each family type
   }
Figure 4) sockaddr structure

Because OS/2 only uses the AF_INET family, the sockaddr structure may be substituted with the sockaddr_in structure. This is just a structure with the 14 data bytes defined for the internet addressing scheme. It is defined in <netinet/in.h> header and looks like this:


struct sockaddr_in
    {
    short  sin_family;       // AF_INET always for OS/2
    ushort sin_port;         // Port to be used in network byte order
    struct in_addr sin_addr; // The internet address to be used.
                             // It is also in network byte order
    char   sin_zero[8];      // Extra junk. Set it to all zeros
    }
Figure 5) sockadd_in structure

Host and Network Byte Orders - Yet Another Brief Aside

Machines on a network can have different ways of storing integers. Intel machines, such as the PC we run OS/2 on, use "little endian", while Motorola and SPARC machines use "big endian". Sockets require that all data coming in and going out be in network byte order, which is most significant byte first or "big endian". There are a couple of socket utility functions used to convert integers from little to big endian and vise-versa. The naming scheme for most of these functions makes it very easy to remember them so rather than listing all of the functions I will only explain the scheme.

The function names are composed of 4 parts. First is the byte order to translate from. This is either n or h for network or host respectively. Second is just "to". Third is the byte order to translate to. This is also either n or h. Last is the type of integer being translated. It will be l or s for a long or short integer. The functions all return the same integer type as what they are converting. For instance:

To convert a port number to network byte order, which must be done for the port in the sockaddr_in structure, you would use the function htons().

There are also two functions which can be used to swap the bytes in an integer. These are bswap() and lswap() for short and long integer respectively. All of these functions are defined in .

Back to the socket API


int soclose
   (
   int s  // Descriptor of the socket to be closed
   )
Figure 6) soclose() function

The soclose() function is used to shut a socket down, free its resources, and close the TCP connection if one is open. It will return 0 if successful and -1 if it failed.

Connection Management

These functions are used by clients and server to initiate and manage socket connections from one another.


int connect
  (
  int  s,                // The socket descriptor for your connection
  struct sockaddr *name, // The socket address structure for the socket
                         // you wish to connect to
  int namelen            // Size, in bytes, of the socket address
                         // pointer to name
  )
Figure 7) connect() function

connect() is one of the mainstay functions in a socket program. You use it with a clients to connect between two machines on a network. Connect will actually perform two tasks when it is called. The first is to complete any binding necessary for a stream socket (in case you haven't already used bind() for this socket). Second, It attempts to make the connection to the host supplied in the sockaddr structure.

In order for connect() to succeed, the point to which it is attempting to connect must have a passive open socket pending. In other words, the server had better be listening after calling bind() and listen(). If this is not the case, connect() will return a -1.

Depending on how the socket you supply in s is configured, connect() will either block or not block. Using our unmodified sockets for now, we will assume that connect() is always in blocking mode. This means that while connect() is trying to connect the program or thread will not continue until a connection is established or an error is received. I will cover the non-blocking situation in a future article.


int listen
   (
   int s,       // Socket descriptor
   int backlog  // Number of pending connections allowed
   )
Figure 8) listen() function

The listen() call applies only to stream sockets and performs two tasks. It will complete any binding if necessary (i.e. you forgot to do it) and then it will create a connection request queue of length backlog. After this queue is full, no further connections will be accepted.

listen() is called when your server is ready to begin accepting connections from clients. You must first allocate a stream socket using the socket() function then bind that socket to the socket address structure (or name) of the server. Listen must be called before you can accept any connections.

Backlog can be any number between 0 and SOMAXCONN. SOMAXCONN is defined in <sys/socket.h>.


int accept
   (
   int s,                  // The socket for accepting new connections
   struct sockaddr *name,  // Pointer to a buffer which will hold the
                           //name information for the connecting client
   int *namelen            // Initially points to an integer which
                           // contains the size of the buffer name points
                           // to. On return, it contains the size of the
                           // struct name now points to
   )
Figure 9) accept() function

accept() is what is used by a server to accept the incoming connection requests from a client. The first connection request in the queue set up by listen() is the connection which is accepted. A new socket will be created by accept() with the same properties as socket s. It is this socket which accept() will return. If there are no pending connection requests then accept() will block until a request is made. Non-blocking will be covered in a later article. The new socket which is returned cannot be used to accept new connections; however, s is still available to accept more requests.

The sockaddr structure in the parameter list is used to store the socket address structure of the connection requester. This parameter can be NULL if you have no need for this information. I find it handy to have this information so I advise you to go ahead and grab it even if you don't plan on using it. If you do get lazy and set the name to NULL then you don't have to worry about the namelen parameter either.


int shutdown
   (
   int s,   // Socket to be shutdown
   int how  // Method of shutdown
   )
Figure 10) shutdown() function shutdown() is used to shutdown all or part of a duplex connection. In English, you can shutdown communication from socket s, to socket s, or both to and from socket s depending on the value of how. Those values are 0, 1, and 2 respectively.

Connected Data Exchanges

These functions are for transferring data to and from connected sockets. There are two more functions for receiving and sending data which I will cover later. They are a bit harder to handle than the two I have included here.


int recv
   (
   int s,       // Socket to receive data from
   char *buf,   // Pointer to a buffer to receive the data into
   int len,     // Length of buffer pointed to by buf in bytes
   int flags    // Flags for receiving the data
   )
Figure 11) recv() function

recv() is used to receive data from a connected socket. If successful it will return the length, in bytes, of the message packet. A value of 0 indicates the connection is closed and -1 indicates an error.

recv() will block, if the socket is in blocking mode, until a message arrives and then places the incoming data into the buffer and returns its length. If the incoming data is too large to fit into the buffer than the socket will read up to the parameter specified in len. There is a method to check and see the amount of data waiting to be read but it is part an advanced topic I don't want to sling at you right now.

There are two parameters you can "OR" together to modify how recv() reads the incoming data. The first, MSG_OOB, is for datagram socket and will not be covered. The other is MSG_PEEK. This flag allows you 'peek' at the data without consuming it. This means the data is still waiting to be fully read in from the socket. Normally, NULL is used in this parameter.


int send
   (
   int s,     // Socket to send data on
   char *msg, // Pointer to a buffer containing  data to be sent
   int len,   // Size, in bytes, of data which msg points to
   int flags  // Flags for sending data
   )
Figure 12) send() function

send() is recv()'s counterpart. It operates in the same manner as recv() except it sends the data through the socket. send() will send all of the data in msg as long as it doesn't exceed the total socket buffer space. If is exceeds this buffer size then send will block until all of the data can be sent. This will only occur if you send a large amount of data rapidly through the socket.

Both of the flags which can be set for send() are not normally used; therefore, I will skip them. Use a 0 in the flag parameter when using this function call.

This wraps up the discussion on what I feel are the minimal functions needed to set up a very basic client and server. At this point you should have a basic understanding of how to program a sockets based application.

Finish Up and Next Month

I hope this article will help you to get started in programming with sockets. It may seem like there is a lot of information needed to program with sockets; however, once this slightly steep learning curve is past it becomes second nature to program sockets quickly and easily.

Next month I will be adding more features to the simple client/server program and discuss more of the socket utility API. Until then I urge you to read over my sample code and play with the two programs to get a good feel of how they are working. Thanks for reading.. See you next month!