翻译前言:
本文选自Linux Journal 的一篇系列文章Linux Network Programming,并作了部分修改。你可能会说网上关于套接口编程的文章一大堆,但是这是我的第一篇译作,对我来说这是一次新的尝试,有什么意见或建议欢迎与我联系:hellwolf_ok@sina.com
Linux网络编程,第一部分
BSD套接口篇
——这是关于如何用各种可以得到的接口为Linux开发网络程序的系列文章的第一篇。就像大多数Unix-based的操作系统一样,Linux支持将TCP/IP作为本地的网络传输协议。在这个系列中,我们假定你已经比较熟悉Linux上的C编程和Linux的一些系统知识诸如signals,forking等等。
这篇文章是关于如何用BSD套接口创建网络程序的基础介绍 。在下一篇中,我们会解决涉及到建立(网络)deamon进程的问题。而且今后的文章我们还会涉及到使用远程过程调用(RPC),以及用CORBA/distributed objects进行开发。
一、TCP/IP的基础介绍
TCP/IP协议族允许两个运行在同一台电脑或者由网络连接在一起的两台电脑上的程序进行通讯。这个协议族是专门为了在不可靠的网络上进行通讯设计的。TCP/IP允许两个基本的操作模式——面向连接的可靠的传输(指TCP)和无连接的(connectionless)不可靠的传输(UDP)。下图举例说明了TCP/IP协议族的分层结构。
TCP提供带有对上层协议透明的中继功能的,顺序的,可靠的,双向的(bi-directional),以连接为基础的字节传输流。TCP将你的信息分割成数据报(不大于64kb)并保证所有的数据报无误的按照顺序都到达目的地。由于以连接为基础,所以一个虚拟连接必须在一个网络实体(network entity)和另一个之间进行通信前建立。UDP相反则提供一个(非常快的)无连接的不可靠消息传输(消息的大小是一个确定的最大长度)。
为了使程序间可以相互通信,不论他们是在同一个机器(通过loopback接口)还是不同主机,每一个程序都必须有独立的地址。
TCP/IP地址由两部分组成——用来辨别机器的IP地址和用来辨别在那台机器上的特定程序的端口地址。
地址可以是点分(dotted-quad)符号形式的(如,127.0.0.1)或者是主机名形式的(如,www.csdn.net)。系统可以使用/etc/hosts或DNS域名服务(如果可以获得的话)进行主机名到点分符号地址(也就是IP地址)的转换。
端口从1号开始编号。1和IPP0RT_RESERVED(在/usr/include/netinet/in.h中定义,通常为1024)之间的段口号保留给系统使用(也就是说,你必须以root的身份建立一个网络服务来绑定这部分的端口)。
最简单的网络程序大都用的客户-服务器模型。一个服务进程等待一个客户进程连接他。当连接建立时,服务器代表客户执行特定的任务,通常这这以后连接就中断了。
二、使用BSD套接口界面
最通行的TCP/IP编程方法就是使用BSD套接口界面编程。通过它,网络端点(network endpoints)(IP地址和端口地址)以套接口(sockets)的形式出现。
这套套接口IPC(interprocess communication,进程间通讯)设施(从4.2BSD开始引入)的设计是为了能让网络程序的设计能够独立于不同的底层通信设施。
1、建立一个服务器程序
要使用BSD界面建立一个服务器程序,你必须通过以下步骤:
1。通过函数socket()建立一个套接口
2。通过函数bind()绑定一个地址(IP地址和端口地址)。这一步确定了服务器的位置,使客户端知道如何访问。
3。通过函数listem()监听(listen)端口的新的连接请求。
4。通过函数accept()接受新的连接。
通常,维护代表了客户的请求可能需要花费相当长的一段时间。在处理一个请求时,接收和处理新的请求也应该是高效的。达到这种目的的最通常的做法是让服务器通过fork()函数拷贝一份自己的进程来接受新的连接。(下图:客户服务器代码模型的表示形式)
以下的例子显示了服务器是如何用C实现的:
/*
* Simple "Hello, World!" server
* Ivan Griffin (ivan.griffin@ul.ie)
*/
/* Hellwolf Misty translated */
#include /* */
#include /* exit() */
#include /* memset(), memcpy() */
#include /* uname() */
#include
#include /* socket(), bind(),
listen(), accept() */
#include
#include #include #include /* fork(), write(), close() */ /* * constants */const char MESSAGE[] = "Hello, World!\n";const int BACK_LOG = 5; /* *程序需要一个命令行参数:需要绑定的端口号 */int main(int argc, char *argv[]){ int serverSocket = 0, on = 0, port = 0, status = 0, childPid = 0; struct hostent *hostPtr = NULL; char hostname[80] = ""; struct sockaddr_in serverName = { 0 }; if (2 != argc) { fprintf(stderr, "Usage: %s \n", argv[0]); exit(1); } port = atoi(argv[1]);/ * *socket()系统调用,带有三个参数: * 1、参数domain指明通信域,如PF_UNIX(unix域),PF_INET(IPv4), * PF_INET6(IPv6)等 * 2、type指明通信类型,最常用的如SOCK_STREAM(面向连接可靠方式, * 比如TCP)、SOCK_DGRAM(非面向连接的非可靠方式,比如UDP)等。 * 3、参数protocol指定需要使用的协议。虽然可以对同一个协议 * 家族(protocol family)(或者说通信域(domain))指定不同的协议 * 参数,但是通常只有一个。对于TCP参数可指定为IPPROTO_TCP,对于 * UDP可以用IPPROTO_UDP。你不必显式制定这个参数,使用0则根据前 * 两个参数使用默认的协议。 */ serverSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (-1 == serverSocket) { perror("socket()"); exit(1); } /* * 一旦套接口被建立,它的运作机制可以通过套接口选项(socket option)进行修改。 */ /* * SO_REUSEADDR选项的设置将套接口设置成重新使用旧的地址(IP地址加端口号)而不等待 * 注意:在Linux系统中,如果一个socket绑定了某个端口,该socket正常关闭或程序退出后, * 在一段时间内该端口依然保持被绑定的状态,其他程序(或者重新启动 的原程序)无法绑定该端口。 * * 下面的调用中:SOL_SOCKET代表对SOCKET层进行操作 */ on = 1; status = setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, (const char *) &on, sizeof(on)); if (-1 == status) { perror("setsockopt(...,SO_REUSEADDR,...)"); } /* 当连接中断时,需要延迟关闭(linger)以保证所有数据都 * 被传输,所以需要打开SO_LINGER这个选项 * linger的结构在/usr/include/linux/socket.h中定义: * struct linger * { * int l_onoff; /* Linger active */ * int l_linger; /* How long to linger */ * }; * 如果l_onoff为0,则延迟关闭特性就被取消。如果非零,则允许套接口延迟关闭。 * l_linger字段则指明延迟关闭的时间 */ { struct linger linger = { 0 }; linger.l_onoff = 1; linger.l_linger = 30; status = setsockopt(serverSocket, SOL_SOCKET, SO_LINGER, (const char *) &linger, sizeof(linger)); if (-1 == status) { perror("setsockopt(...,SO_LINGER,...)"); } } /* * find out who I am */ status = gethostname(hostname, sizeof(hostname)); if (-1 == status) { perror("gethostname()"); exit(1); } hostPtr = gethostbyname(hostname); if (NULL == hostPtr) { perror("gethostbyname()"); exit(1); } (void) memset(&serverName, 0, sizeof(serverName)); (void) memcpy(&serverName.sin_addr, hostPtr->h_addr, hostPtr->h_length); /* *h_addr是h_addr_list[0]的同义词, * h_addr_list是一组地址的数组 *长度为4(byte)代表一个IP地址的长度 *//* * 为了使服务器绑定本机所有的IP地址, * 上面一行代码需要用下面的代码代替 * serverName.sin_addr.s_addr=htonl(INADDR_ANY); */ serverName.sin_family = AF_INET; /* htons:h(host byteorder,主机字节序) * to n(network byteorder,网络字节序 * s(short类型) */ serverName.sin_port = htons(port);/* 在一个地址(本例中的serverSocket)被建立后 * 它就应该被绑定到我们获得的套接口。 */ status = bind(serverSocket, (struct sockaddr *) &serverName, sizeof(serverName)); if (-1 == status) { perror("bind()"); exit(1); }/* 现在套接口就可以被用来监听新的连接。 * BACK_LOG指定了未决连接监听队列(listen queue for pending connections) * 的最大长度。当一个新的连接到达,而队列已满的话,客户就会得到连接拒绝错误。 * (这就是dos拒绝服务攻击的基础)。 */ status = listen(serverSocket, BACK_LOG); if (-1 == status) { perror("listen()"); exit(1); }/* 从这里开始,套接口就开始准备接受请求,并为他们服务。 * 本例子是用for循环来达到这个目的。一旦连接被接受(accpepted), * 服务器可以通过指针获得客户的地址以便进行一些诸如记录客户登陆之类的 * 任务。 for (;;) { struct sockaddr_in clientName = { 0 }; int slaveSocket, clientLength = sizeof(clientName); (void) memset(&clientName, 0, sizeof(clientName)); slaveSocket = accept(serverSocket, (struct sockaddr *) &clientName, &clientLength); if (-1 == slaveSocket) { perror("accept()"); exit(1); } childPid = fork(); switch (childPid) { case -1: /* ERROR */ perror("fork()"); exit(1); case 0: /* child process */ close(serverSocket); if (-1 == getpeername(slaveSocket, (struct sockaddr *) &clientName, &clientLength)) { perror("getpeername()"); } else { printf("Connection request from %s\n", inet_ntoa(clientName.sin_addr)); } /* * Server application specific code * goes here, e.g. perform some * action, respond to client etc. */ write(slaveSocket, MESSAGE, strlen(MESSAGE)); /* 也可以使用带缓存的ANSI函数fprint, * 只要你记得必要时用fflush刷新缓存 */ close(slaveSocket); exit(0); default: /* parent process */ close(slaveSocket);/* 这是一个非常好的习惯 * 父进程关闭子进程的套接口描述符 * 正如上面的子进程关闭父进程的套接口描述符。 */ } } return 0;}