(接上篇)
2、建立对应的客户端
正如你正要看到的,相比服务端,客户端的代码就要简单多了。在这个程序中你必须提供两个命令行参数:服务端所在机器主机名或IP地址,和服务段绑定的端口。当然,服务端还必须在客户端运行以前就已经正常运行:P。
/*
* Listing 2:
* An example client for "Hello, World!" server
* Ivan Griffin (ivan.griffin@ul.ie)
*/
/* Hellwolf Misty translated */
#include <stdio.h> /* perror() */
#include <stdlib.h> /* atoi() */
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h> /* read() */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
int main(int argc, char *argv[])
{
int clientSocket,
remotePort,
status = 0;
struct hostent *hostPtr = NULL;
struct sockaddr_in serverName = { 0 };
char buffer[256] = "";
char *remoteHost = NULL;
if (3 != argc)
{
fprintf(stderr, "Usage: %s
\n",
argv[0]);
exit(1);
}
remoteHost = argv[1];
remotePort = atoi(argv[2]);
clientSocket = socket(PF_INET, SOCK_STREAM,
IPPROTO_TCP);
if (-1 == clientSocket)
{
perror("socket()");
exit(1);
}
/*
* 首先假定是DNS主机名
* 注:
* struct hostent{
* char *h_name; /* official name of host */
* char **h_aliases; /* alias list */
* int h_addrtype; /* host address type */
* int h_length; /* length of address */
* char **h_addr_list; /* list of addresses from name server */
* };
* #define h_addr h_addr_list[0]
* 注意到了吗?h_addr是一个宏,如果你用gdb调试时
* display phostent->h_addr出错的话不要奇怪
*/
hostPtr = gethostbyname(remoteHost); /* struct hostent *hostPtr; */
if (NULL == hostPtr)
{/* 不是?? */
hostPtr = gethostbyaddr(remoteHost,
strlen(remoteHost), AF_INET);/* 应该是点分形式的IP地址吧*/
if (NULL == hostPtr) /* 还不是,!-_- */
{
perror("Error resolving server address");
exit(1);
}
}
serverName.sin_family = AF_INET;
serverName.sin_port = htons(remotePort);
(void) memcpy(&serverName.sin_addr,
hostPtr->h_addr,
hostPtr->h_length);
/* 这里并不需要再bind了,因为connect已经可以为我们解决一切 */
status = connect(clientSocket,
(struct sockaddr*) &serverName,
sizeof(serverName));
if (-1 == status)
{
perror("connect()");
exit(1);
}
/* connect成功后,一个双工(duplex)的网络连接就被建立好了
* 像服务器一样,客户端可以使用read()和write()接收数据
/*
* 客户端的具体代码应该从这里开始实施
* 比如从服务端接受和回应信息等等
*/
while (0 < (status = read(clientSocket,
buffer, sizeof(buffer) - 1)))
{
printf("%d: %s", status, buffer);
/* 注:如果读成功,status表示获得的字节数(包括'\0') */
}
if (-1 == status)
{
perror("read()");
}
close(clientSocket);
return 0;
}
需要注意的几点:
发送文字通常都工作正常。但是记住不同的系统对换行的实现有差别(比如,Unix使用\0x12,而微软使用\0x15\0x12)。
不同的实现可能使用不同的字节序(byte-order)。但不用担心,BSD的设计者们在一考虑了这一点。有很多现成的函数实现了这种转换,他们都有一定的命名规则:htons代表实现host-to-network的short结构的转换,还有htonl,ntohs,ntohl,也很容易判断出他们的工作方式。至于网络字节序是大端对齐(big-endian)还是小端对齐(little-endian)并不是个问题,因为在TCP/IP网络上它已经被标准化了(注:网络字节序用的是大端对齐)。当然除非你一直在网络上发一个字符(注:还得是ASCII的),不用这些转换函数不会引起大问题,但通常情况下,你会遭遇到字节序问题。这还要看你的机器,有时侯这些函数就是一个空宏,有时候它们确实是函数。有趣的是,最常见的网络编程的bugs来源是忘记了在填充sockaddr_in结构的sin_addr.s_addr字段时忘记了使用这些函数,即使使用INADDR_ANY也必须这么做。 网络编程的一个重要目标就是不给双方带来不可预料的麻烦。比如说,服务器在访问关键数据时必须通过必要的机制同步对这部分资源的访问,避免由此带来的死锁并且保证数据的有效性。 大多数情况下,你不能从机器间传递指针并试图使用它。
<>类似的,大多数情况下,你也不能试图通过套接口传递一个文件描述符从一个进程到另一个进程(非子进程)并直接使用它。BSD和SVR4系统提供了在不相关的进程间传递文件描述符的不同方法;然而,在linux下最简单的方法就是通过使用/proc文件系统。
此外,你必须保证你正确的解决了short writes问题。Short write发生在write()调用仅仅部分的将缓冲区写给一个文件描述符对应的设备。它们发生的原因归咎于操作系统的缓冲区,和底层传输系统的流控制系统。某些系统调用,通常被称作慢系统调用(slow system calls)可能会被(其他调用)中断。一些可能不会被自动重起,所以你必须明确地在网络编程时解决这一问题。下面的代码解决了short write问题:
/*
* Listing 3:
* Handling short writes
* Ivan Griffin (ivan.griffin@ul.ie)
*/
/* Hellwolf Misty translated */
int bytesToSend = 0,
bytesWritten = 0,
num = 0; /*
* 这里用到的bytesToSend, buffer, and
* fileDesc必须已经在其他某个地方有定义.
*/
for (bytesWritten = 0; bytesWritten < bytesToSend;
bytesWritten += num)
{
num = write(fileDesc,
(void *)( (char *)buffer +
(char *)bytesWritten ),
bytesToSend - bytesWritten);
if (num < 0)
{
perror("write()");
if (errno != EINTR)
{
exit(1);
}
}
}
使用多线程而不是多进程可能会减轻服务器的负担,并且更加有效。线程间上下文的转化(当然是指同一个进程空间)通常开销比进程间上下文转换小得多。然而,如此多的子线程都在操作网络I/O,如果它们在内核级还可以,但如果它们是用户级的,整个进程都会因为第一个调用I/O的线程而阻塞。这将会导致不愿看到的其他线程的饥饿状态直到I/O的完成。正如你看到的,当使用简单的forking模型时在父进程和子进程中关闭不必要的套接口文件描述符是相当寻常的。这保护了进程潜在的错误读写这些描述符的可能性。但是不要试图在使用线程模型时这样做,进程中的多线程共享同一个内存虚拟地址空间和文件描述符集。如果你在一个线程中关闭了一个描述符,那么进程中的其他所有的线程都将无法得到该描述符。
3、无连接的数据传输——UDP
下面的代码显示了一个使用UDP的服务端。UDP程序很像TCP程序,但他们又很大的不同。首先,UDP不保证可靠的传输——如果你需要在使用UDP时获得可靠性,你必须或者自己实现或者转而用TCP。
像TCP程序一样,用UDP你可以建立一个套接口并将其绑定到特定地址。UDP服务端不监听(listen)和接受(accept)外来的连接,客户也不必显式的连接到服务器。事实上,在UDP客户端和服务段之间并没有太大的区别。服务端必须绑定到一个确定的端口和地址好让客户端知道向哪里发送数据。而且当你的服务端使用send(),客户端也应该使用对应的recv族函数。
UDP服务端程序清单:
/*
* Listing 4:
* Example UDP (connectionless) server
* Ivan Griffin (ivan.griffin@ul.ie)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define MAX_MESG_SIZE 4096
char mesg[MAX_MESG_SIZE] = "";
int main(int argc, char *argv[])
{
int udpSocket = 0,
myPort = 0,
status = 0,
size = 0,
clientLength = 0;
struct sockaddr_in serverName = { 0 },
clientName = { 0 };
if (2 != argc)
{
fprintf(stderr, "Usage: %s \n",
argv[0]);
exit(1);
}
myPort = atoi(argv[1]);
udpSocket = socket(PF_INET, SOCK_DGRAM, /* PF_INET和SOCK_DGRAM组合代表了UDP */
IPPROTO_UDP);
if (-1 == udpSocket)
{
perror("socket()");
exit(1);
}
memset(&serverName, 0, sizeof(serverName));
memset(&clientName, 0, sizeof(clientName));
serverName.sin_family = AF_INET;
serverName.sin_addr.s_addr = htonl(INADDR_ANY);
serverName.sin_port = htons(myPort);
status = bind(udpSocket, (struct sockaddr *)
&serverName, sizeof(serverName));
if (-1 == status)
{
perror("bind()");
exit(1);
}
for (;;)
{
/* ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sock-
* addr *from, socklen_t *fromlen);
* /
size = recvfrom(udpSocket, mesg,
MAX_MESG_SIZE, 0,
(struct sockaddr *) &clientName,
&clientLength);
if (size == -1)
{
perror("recvfrom()");
exit(1);
}
/* ssize_t sendto(int s, const void *msg, size_t len, int flags, const
* struct sockaddr *to, socklen_t tolen);
*/
status = sendto(udpSocket, mesg, size, 0,
(struct sockaddr *) &clientName,
clientLength);
if (status != size)
{
fprintf(stderr,
"sendto(): short write.\n");
exit(1);
}
}
/* never reached */
return 0;
}
将TCP的客户端改写成UDP的客户端将留作一个练习
4、/etc/services文件
为了连接到一个服务端,你必须首先知道其监听的地址和端口。许多常见的服务(FTP,TELNET等等)的信息都列在了一个文本文
件/etc/services中。getservbyname()函数可以用名称询问一个服务的详细情况包括它的端口号(注意!它已经是网络字节序了),它的原型在
/usr/include:
struct servent *getservbyname(const char *name, const char *proto); 默认的proto是"tcp"
struct servent
{
char *s_name; /* official service name */
char **s_aliases; /* alias list */
int s_port; /* port number, network
* byte-order--so do not
* use host-to-network macros */
char *s_proto; /* protocol to use */
};
5、总结
这篇文章介绍了Linux下使用C和BSD Socket API进行网络编程。总的来说,用这套API来写代码将是相当耗费劳力的,特别是与其他一些
技术相比。在今后的文章中,我会将BSD Socket API与另外两个可以选择的Linux下的方法进行比较:远程过程调用(RPCs,Remote Procedure
Calls)和CORBA(Common Object Request Broker Achitecture,公用对象请求代理[调度]程序体系结构)。RPCs在Ed Petron的文章"Remote
Procedure Calls"(Linux Journal Issue #42,1997年十月)中有介绍。
相关资源:
UNIX Network Programming, W. Richard Steves, Prentice Hall, 1990.
An Introductory 4.4BSD Interprocess Communication Tutorial, Stuart Sechrest, University of California, Berkeley. 1986.
Available via anonymous FTP at: ftp://ftp.NetBSD.ORG/pub/NetBSD/misc/lite2-docs/psd/20.ipctut.ps.gz.
An Advanced 4.4BSD Interprocess Communication Tutorial, Samuel J. Leffler, Robert S. Fabry, William N. Joy, Phil Lapsley,
University of California, Berkeley. 1986 Available via anonymous FTP at: ftp://ftp.NetBSD.ORG/pub/NetBSD/misc/lite2-
docs/psd/21.ipc.ps.gz.
Java and the Client-Server, Joe Novosel, Linux Journal, Issue 33, January 1997.
RFC 793: Transmission Control Protocol, J. Postel (ed.), September 1981. Available via HTTP at
http://www.internic.net/rfc/rfc793.txt.
RFC 1337: TIME-WAIT Assassination Hazards in TCP, R. Braden, May 1992. Available via HTTP at
http://www.internic.net/rfc/rfc1337.txt.
Programming UNIX Sockets in C FAQ, Vic Metcalfe, Andrew Gierth and other contributers, February 1997. Available via HTTP at
http://kipper.york.ac.uk/~vic/sock-faq/html/unix-socket-faq.html
作者简介:
Ivan Griffin is a research postgraduate student in the ECE department at the University 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). His e-mail address is ivan.griffin@ul.ie.
Dr. John Nelson is a senior lecturer in Computer Engineering at the University of Limerick. His interests include mobile
communications, intelligent networks, Software Engineering and VLSI design. His e-mail address is john.nelson@ul.ie.
所有的程序都可以在这里获得: ftp://ftp.ssc.com/pub/lj/listings/issue46/2333.tgz