Linux网络编程

来源:互联网 发布:yuntv外国直播软件 编辑:程序博客网 时间:2024/04/25 18:54

此文是从 http://www.linuxjournal.com翻译过来的,见http://www.linuxjournal.com/article/2333
Likemost other Unix-based operating systems, Linux supports TCP/IP as itsnative network transport. In this series, we will assume you are fairlyfamiliar with C programming on Linux and with Linux topics such assignals, forking, etc.

就像大多数其它基于UNIX的操作系统,linux支持TCP/IP协议作为本地网络传输.在此,我们假设你已经熟悉在linux进行C编程和其它话题如信号,进程等等.

This article is a basic introduction to using the BSD socket interfacefor creating networked applications. In the next article, we will dealwith issues involved in creating (network) daemon processes. Futurearticles will cover using remote procedure calls and developing withCORBA/distributed objects.

本文用BSD socket 接口进行网络编程的一个入门.在下一文中,我们会讨论有关网络守护进程的内容.之后的文章中会涉及远程调用和用CORBA/分布式对象编程

Brief Introduction to TCP/IP
TCP/IP简述

TheTCP/IP suite of protocols allows two applications, running on eitherthe same or separate computers connected by a network, to communicate.It was specifically designed to tolerate an unreliable network. TCP/IPallows two basic modes of operation—connection-oriented, reliabletransmission and connectionless, unreliable transmission (TCP and UDPrespectively). Figure 1 illustrates the distinct protocol layers in theTCP/IP suite stack.

TCP/IP协议允许两个程序远行于网络相连的相同或不同机器中进行交流.这设计主要是弥补不可靠的网络.

Figure 1. TCP/IP Protocol Layers

TCPprovides sequenced, reliable, bi-directional, connection-basedbytestreams with transparent retransmission. In English, TCP breaksyour messages up into chunks (not greater in size than 64KB) andensures that all the chunks get to the destination without error and inthe correct order. Being connection-based, a virtual connection has tobe set up between one network entity and the other before they cancommunicate. UDP provides (very fast) connectionless, unreliabletransfer of messages (of a fixed maximum length).

TCP 提供了有序可靠定向的基干字节流透明重发.TCP 把你的消息(小于64K)记录在一个块中,保证无错有序地发送到目的.基于连接,两个实体已经建立虚拟连接.UDP 提供(很快)无连接,不可靠的消息传输.

Toallow applications to communicate with each other, either on the samemachine (using loopback) or across different hosts, each applicationmust be individually addressable.

为了让程序相互交流,不论是本地连接或远程连接,每个程序都有相应的地址.

TCP/IPaddresses consist of two parts—an IP address to identify the machineand a port number to identify particular applications running on thatmachine.

TCP/IP 地址由两部分组成---一个IP地址标识一个机器和一个端口号标识机器运行中的程序.

The addresses are normally given in either the “dotted-quad” notation (i.e., 127.0.0.1) or as a host name (foobar.bundy.org). The system can use either the /etc/hosts file or the Domain Name Service (DNS) (if available) to translate host names to host addresses.

地址可以是记号法(如 127.0.0.1)或是一个主机名(如,name.org).系统可以用/etc/hosts 文件或用域名服务器(DNS)(可以的话)翻译主机名为主机地址.

Portnumbers range from 1 upwards. Ports between 1 and IPPORT_RESERVED(defined in /usr/include/netinet/in.h—typically 1024) are reserved forsystem use (i.e., you must be root to create a server to bind to theseports).

端口号从一开始.端口从一到IPPORT_RESERVED(在/usr/include/netinet/in.h定义,一般为1024)供系统留用

The simplest network applications follow the client-servermodel. A server process waits for a client process to connect to it.When the connection is established, the server performs some task onbehalf of the client and then usually the connection is broken.

最简单的网络程序为客户-服务模型.服务端等待客户端连接.一旦建立连接,服务程序客户端执行一些任务然后连接破开.

Using the BSD Socket Interface
使用BSD Socket 接口

The most popular method of TCP/IP programming is to use the BSD socket interface. With this, network endpoints (IP address and port number) are represented as sockets.

最流行的TCP/IP编程方法是用BSD Socket接口.网络终端(IP地址和端口号)作为sockets.

Thesocket interprocess communication (IPC) facilities (introduced with4.2BSD) were designed to allow network-based applications to beconstructed independently of the underlying communication facilities.

scoket 进程间通信设备(4.2BSD导入)的设计允许基于网络程序建设在底层的通讯设备接口.
Creating a Server Application
建立一个服务程序

To create a server application using the BSD interface, you must follow these steps:

为了用BSD接口建立一个服务程序,你必须跟着下列步骤.

  1. Create a new socket by typing: socket().

  2. 输入socket()建立一个新接口.
  3. bind an address (IP address and port number) to the socket by typing: bind. This step identifies the server so that the client knows where to go.

  4. 输入bind绑定一个地址(IP地址和端口号)到一个socket.这一步骤让服务程序知道客户在哪里.
  5. listen for new connection requests on the socket by typing: listen().

  6. 输入listen()在socket上监听连接请求.
  7. accept new connections by typing: accept().

  8. 输入accept()接受新连接.

Often,the servicing of a request on behalf of a client may take aconsiderable length of time. It would be more efficient in such a caseto accept and deal with new connections while a request is beingprocessed. The most common way of doing this is for the server to fork a new copy of itself after accepting the new connection.

通常,客户的连接请求须占用多量时间.这种情况下当请求已经处理去接受和处理新连接会更有效.通常的做法是用fork去复制新进程去接受新连接.

Figure 2. Representation of Client/Server Code

客户/服务程序代码

The code example in Listing 1shows how servers are implemented in C. The program expects to becalled with only one command-line argument: the port number to bind to.It then creates a new socket to listen on using the socket() system call. This call takes three parameters: the domain in which to listen to, the socket type and the network protocol.

Listing 1 中的示例代码显示如何用C实现服务程序.程序接受一个命令行参数:连接的端口号.然后建立一个新的socket用socket()去监听系统调用.这个调用接受三个参数:监听的域名,socket类型,和网络协议.

 

Thedomain can be either the PF_UNIX domain (i.e., internal to the localmachine only) or the PF_INET (i.e., all requests from the Internet).The socket type specifies the communication semantics of theconnection. While a few types of sockets have been specified, inpractice, SOCK_STREAM and SOCK_DGRAM are the most popularimplementations. SOCK_STREAM provides for TCP reliableconnection-oriented communications, SOCK_DGRAM for UDP connectionlesscommunication.

The protocolparameter identfies the particular protocol to be used with the socket.While multiple protocols may exist within a given protocol family (ordomain), there is generally only one. For TCP this is IPPROTO_TCP, forUDP it is IPPROTO_UDP. You do not have to explicitly specify thisparameter when making the function call. Instead, using a value of 0will select the default protocol.

Once the socket is created, itsoperation can be tweaked by means of socket options. In the aboveexample, the socket is set to reuse old addresses (i.e., IP address +port numbers) without waiting for the required connection closetimeout. If this were not set, you would have to wait four minutes inthe TIME_WAIT state before using the address again. The four minutescomes from 2 * MSL. The recommended value for MSL, from RFC 1337, is120 seconds. Linux uses 60 seconds, BSD implementations normally usearound 30 seconds.

The socket can linger to ensure that all datais read, once one end closes. This option is turned on in the code. Thestructure of linger is defined in /usr/include/linux/socket.h. It looks like this:

struct linger
{
int l_onoff; /* Linger active */
int l_linger; /* How long to linger */
};

If l_onoff is zero, lingering is disabled. If it is non-zero, lingering is enabled for the socket. The l_linger field specifies the linger time in seconds.

The server then tries to discover its own host name. I could have used the gethostname() call, but the use of this function is deprecated in SVR4 Unix (i.e., Sun's Solaris, SCO Unixware and buddies), so the local function _GetHostName() provides a more portable solution.

Oncethe host name is established, the server constructs an address for thesocket by trying to resolve the host name to an Internet domainaddress, using the gethostbyname()call. The server's IP address could instead be set to INADDR_ANY toallow a client to contact the server on any of its IP addresses—used,for example, with a machine with multiple network cards or multipleaddresses per network card.

After an address is created, it isbound to the socket. The socket can now be used to listen for newconnections. The BACK_LOG specifies the maximum size of the listenqueue for pending connections. If a connection request arrives when thelisten queue is full, it will fail with a connection refused error.[This forms the basis for one type of denial of service attack —Ed.]See sidebar on TCP listen() Backlog.

Havingindicated a willingness to listen to new connection requests, thesocket then prepares to accept the requests and service them. Theexample code achieves this using an infinite for()loop. Once a connection has been accepted, the server can ascertain theaddress of the client for logging or other purposes. It then forks achild copy of itself to handle the request while it (the parent)continues listening for and accepting new requests.

The child process can use the read() and write()system calls on this connection to communicate with the client. It isalso possible to use the buffered I/O on these connections (e.g., fprint()) as long as you remember to fflush() the output when necessary. Alternatively, you can disable buffering altogether for the process (see the setvbuf() (3) man page).

Asyou can see from the code, it is quite common (and good practice) forthe child processes to close the inherited parent-socket filedescriptor, and for the parent to close the child-socket descriptorwhen using this simple forking model.

Creating the Corresponding Client

The client code, shown in Listing 2,is a little simpler than the corresponding server code. To start theclient, you must provide two command-line arguments: the host name oraddress of the machine the server is running on and the port number theserver is bound to. Obviously, the server must be running before anyclient can connect to it.

In the client example (Listing 2), asocket is created like before. The first command-line argument is firstassumed to be a host name for the purposes of finding the server'saddress. If this fails, it is then assumed to be a dotted-quad IPaddress. If this also fails, the client cannot resolve the server'saddress and will not be able to contact it.

Having located the server, an address structure is created for the client socket. No explicit call to bind() is needed here, as the connect() call handles all of this.

Oncethe connect() returns successfully, a duplex connection has beenestablished. Like the server, the client can now use read() and write()calls to receive data on the connection.

Be aware of the following points when sending data over a socket connection:

  • Sending text is usually fine. Remember that different systems can have different conventions for the end of line (i.e., Unix is /012, whereas Microsoft uses /015/012).

  • Differentarchitectures may use different byte-ordering for integers etc.Thankfully, the BSD guys thought of this problem already. There areroutines (htons and nstoh for short integers, htonl and ntohlfor long integers) which perform host-to-network order andnetwork-to-host order conversions. Whether the network order islittle-endian or big-endian doesn't really matter. It has beenstandardized across all TCP/IP network stack implementations. Unlessyou persistently pass only characters across sockets, you will run intobyte-order problems if you do not use these routines. Depending on themachine architecture, these routines may be null macros or may actuallybe functional. Interestingly, a common source of bugs in socketprogramming is to forget to use these byte-ordering routines forfilling the address field in the sock_addr structures. Perhaps it isnot intuitively obvious, but this must also be done when usingINADDR_ANY (i.e., htonl(INADDR_ANY)).

  • A key goal ofnetwork programming is to ensure processes do not interfere with eachother in unexpected ways. In particular, servers must use appropriatemechanisms to serialize entry through critical sections of code, avoiddeadlock and protect data validity.

  • You cannot(generally) pass a pointer to memory from one machine to another andexpect to use it. It is unlikely you will want to do this.

  • Similarly,you cannot (generally) pass a file descriptor from one process toanother (non-child) process via a socket and use it straightaway. BothBSD and SVR4 provide different ways of passing file descriptors betweenunrelated processes; however, the easiest way to do this in Linux is touse the /proc file system.

Additionally, you mustensure that you handle short writes correctly. Short writes happen whenthe write() call only partially writes a buffer to a file descriptor.They occur due to buffering in the operating system and to flow controlin the underlying transport protocol. Certain system calls, termed slowsystem calls, may be interrupted. Some may or may not be automaticallyrestarted, so you should explicitly handle this when networkprogramming. The code excerpt in Listing 3 handles short writes.

Using multiple threads instead of multiple processes may lighten the load on the server host, thereby increasing efficiency. Context-switchingbetween threads (in the same process address space) generally has muchless associated overhead than switching between different processes.However, since most of the slave threads in this case are doing networkI/O, they must be kernel-level threads. If they were user-levelthreads, the first thread to block on I/O would cause the whole processto block. This would result in starving all other threads of any CPUattention until the I/O had completed.

It is common to closeunnecessary socket file descriptors in child and parent processes whenusing the simple forking model. This prevents the child or parent frompotential erroneous reads or writes and also frees up descriptors,which are a limited resource. But do not try this when using threads.Multiple threads within a process share the same memory space and setof file descriptors. If you close the server socket in a slave thread,it closes for all other threads in that process.

Connectionless Data—UDP

Listing 4shows a connectionless server using UDP. While UDP applications aresimilar to their TCP cousins, they have some important differences.Foremost, UDP does not guarantee reliable delivery—if you requirereliability and are using UDP, you either have to implement it yourselfin your application logic or switch to TCP.

Like TCPapplications, with UDP you create a socket and bind an address to it.(Some UDP servers do not need to call bind(), but it does no harm andwill save you from making a mistake.) UDP servers do not listen oraccept incoming connections, and clients do not explicitly connect toservers. In fact, there is very little difference between UDP clientsand servers. The server must be bound to a known port and address onlyso that the client knows where to send messages. Additionally, theorder of expected data transmissions is reversed, i.e., when you senddata using send() in the server, your client should expect to receive data using recv().

Itis common for UDP clients to fill in the sockaddr_in structure with asin_port value of 0. (Note that 0 in either byte-order is 0.) Thesystem then automatically assigns an unused port number (between 1024and 5000) to the client. I'm leaving it as an exercise to the reader toconvert the server in Listing 4 into a UDP client.

/etc/services

Inorder to connect to a server, you must first know both the address andport number on which it is listening. Many common services (FTP,TELNET, etc.) are listed in a text database file called /etc/services.An interface exists to request a service by name and to receive theport number (correctly formatted in network byte-order) for thatservice. The function is getservbyname(),and its prototype is in the header file /usr/include/netdb.h. Thisexample takes a service name and protocol type and returns a pointer tostruct servent.

struct servent
{
char *s_name; /* official service name */
char **s_aliases; /* alias list */
int s_port; /* port number, network</n>
* byte-order--so do not
* use host-to-network macros */
char *s_proto; /* protocol to use */
};
Conclusions

Thisarticle has introduced network programming in Linux, using C and theBSD Socket API. In general, coding with this API tends to be quitelaborious, especially when compared to some of the other techniquesavailable. In future articles, I will compare two alternatives to theBSD Socket API for Linux: the use of Remote Procedure Calls (RPCs) and the Common Object Request Broker Architecture (CORBA). RPCs were introduced in Ed Petron's article “Remote Procedure Calls” in Linux Journal Issue #42 (October, 1997).

Resources

TCP listen() Backlog

Major System Calls The next article in this series will cover the issues involved in developing long-lived network services (daemons) in Linux.

Alllistings referred to in this article are available by anonymousdownload in the file ftp://ftp.ssc.com/lj/listings/issue46/2333.tgz.

Ivan Griffin (ivan.griffin@ul.ie)is a research postgraduate student in the ECE department at theUniversity of Limerick, Ireland. His interests include C++/Java, WWW,ATM, the UL Computer Society (http://www.csn.ul.ie/) and, of course,Linux (http://www.trc.ul.ie/~griffini/linux.html).

Dr. John Nelson (john.nelson@ul.ie)is a senior lecturer in Computer Engineering at the University ofLimerick. His interests include mobile communications, intelligentnetworks, Software Engineering and VLSI design.

原创粉丝点击