当前位置:Linux教程 - Linux - Beej's 网络编程指南 Internet Sockets(2)

Beej's 网络编程指南 Internet Sockets(2)

gethostname()--Who am I?
甚至比 getpeername() 还简单的函数是 gethostname()。他返回你程序所运行的机器的主机名字。然后你可以使用 gethostbyname() 以获得你的机器的 IP 地址。
下面是定义:


#include

int gethostname(char *hostname, size_t size);

参数很简单:hostname 是一个字符数组指针,他将在函数返回时保存主机名。size 是 hostname 数组的字节长度。

函数调用成功时返回 0,失败时返回 -1,并设置 errno。


--------------------------------------------------------------------------------


DNS--You say ""whitehouse.gov"", I say ""198.137.240.100""
如果你不知道 DNS 的意思,那么我告诉你,他代表""域名服务 (Domain Name Service)""。他主要的功能是:你给他一个容易记忆的某站点的地址,他给你 IP 地址(然后你就可以使用 bind(), connect(), sendto() 或者其他函数。)当一个人输入:
$ telnet whitehouse.gov

telnet 能知道他将连接 (connect()) 到 ""198.137.240.100""。

但是这是如何工作的呢? 你可以调用函数 gethostbyname():

#include

struct hostent *gethostbyname(const char *name);

很明白的是,他返回一个指向 struct hostent 的指针。这个数据结构是这样的:
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
#define h_addr h_addr_list[0]

这里是这个数据结构的详细资料: struct hostent:
h_name - Official name of the host.
h_aliases - A NULL-terminated array of alternate names for the host.
h_addrtype - The type of address being returned; usually AF_INET.
h_length - The length of the address in bytes.
h_addr_list - A zero-terminated array of network addresses for the host. Host addresses are in Network Byte Order.
h_addr - The first address in h_addr_list.

gethostbyname() 成功时返回一个指向 struct hostent 的指针,或者是个空 (NULL) 指针。(但是和以前不同,errno 不设置,h_errno 设置错误信息。请看下面的 herror()。)

但是如何使用呢? 这个函数可不象他看上去那么难用。

这里是个例子:

#include
#include
#include
#include
#include
#include

int main(int argc, char *argv[])
{
struct hostent *h;

if (argc != 2) { /* error check the command line */
fprintf(stderr,""usage: getip address "");
exit(1);
}

if ((h=gethostbyname(argv[1])) == NULL) { /* get the host info */
herror(""gethostbyname"");
exit(1);
}

printf(""Host name : %s "", h->h_name);
printf(""IP Address : %s "",inet_ntoa(*((struct in_addr *)h->h_addr)));

return 0;
}

在使用 gethostbyname() 的时候,你不能用 perror() 打印错误信息(因为 errno 没有使用),你应该调用 herror()。

相当简单,你只是传递一个保存机器名的自负串(例如 ""whitehouse.gov"") 给 gethostbyname(),然后从返回的数据结构 struct hostent 中收集信息。

唯一让人迷惑的是打印 IP 地址信息。h->h_addr 是一个 char *,但是 inet_ntoa() 需要的是 struct in_addr。因此,我转换 h->h_addr 成 struct in_addr *,然后得到数据。


--------------------------------------------------------------------------------


Client-Server Background
这里是个客户--服务器的世界。在网络上的所有东西都是在处理客户进程和服务器进程的交谈。举个 telnet 的例子。当你用 telnet (客户)通过 23 号端口登陆到主机,主机上运行的一个程序(一般叫 telnetd,服务器)激活。他处理这个连接,显示登陆界面,等等。



Figure 2. The Client-Server Relationship.

图 2 说明了客户和服务器之间的信息交换。

注意,客户--服务器之间可以使用SOCK_STREAM、SOCK_DGRAM 或者其他(只要他们采用相同的)。一些很好的客户--服务器的例子有 telnet/telnetd、 ftp/ftpd 和 bootp/bootpd。每次你使用 ftp 的时候,在远端都有一个 ftpd 为你服务。

一般,在服务端只有一个服务器,他采用 fork() 来处理多个客户的连接。基本的程序是:服务器等待一个连接,接受 (accept()) 连接,然后 fork() 一个子进程处理他。这是下一章我们的例子中会讲到的。


--------------------------------------------------------------------------------


简单的服务器
这个服务器所做的全部工作是在留式连接上发送字符串 ""Hello, World! ""。你要测试这个程序的话,可以在一台机器上运行该程序,然后在另外一机器上登陆:
$ telnet remotehostname 3490

remotehostname 是该程序运行的机器的名字。

服务器代码:

#include
#include
#include
#include
#include
#include
#include
#include

#define MYPORT 3490 /* the port users will be connecting to */

#define BACKLOG 10 /* how many pending connections queue will hold */

main()
{
int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */
struct sockaddr_in my_addr; /* my address information */
struct sockaddr_in their_addr; /* connector''s address information */
int sin_size;

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror(""socket"");
exit(1);
}

my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(MYPORT); /* short, network byte order */
my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */

if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))
== -1) {
perror(""bind"");
exit(1);
}

if (listen(sockfd, BACKLOG) == -1) {
perror(""listen"");
exit(1);
}

while(1) { /* main accept() loop */
sin_size = sizeof(struct sockaddr_in);
if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
&sin_size)) == -1) {
perror(""accept"");
continue;
}
printf(""server: got connection from %s "",
inet_ntoa(their_addr.sin_addr));
if (!fork()) { /* this is the child process */
if (send(new_fd, ""Hello, world! "", 14, 0) == -1)
perror(""send"");
close(new_fd);
exit(0);
}
close(new_fd); /* parent doesn''t need this */

while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */
}
}

如果你很挑剔的话,一定不满意我所有的代码都在一个很大的 main() 函数中。如果你不喜欢,可以划分得更细点。

你也可以用我们下一章中的程序得到服务器端发送的字符串。


--------------------------------------------------------------------------------


简单的客户程序
这个程序比服务器还简单。这个程序的所有工作是通过 3490 端口连接到命令行中制定的主机,然后得到服务器的字符串。

客户代码:

#include
#include
#include
#include
#include
#include
#include
#include

#define PORT 3490 /* the port client will be connecting to */

#define MAXDATASIZE 100 /* max number of bytes we can get at once */

int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in their_addr; /* connector''s address information */

if (argc != 2) {
fprintf(stderr,""usage: client hostname "");
exit(1);
}

if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */
herror(""gethostbyname"");
exit(1);
}

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror(""socket"");
exit(1);
}

their_addr.sin_family = AF_INET; /* host byte order */
their_addr.sin_port = htons(PORT); /* short, network byte order */
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero), 8); /* zero the rest of the struct */

if (connect(sockfd, (struct sockaddr *)&their_addr,
sizeof(struct sockaddr)) == -1) {
perror(""connect"");
exit(1);
}

if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {
perror(""recv"");
exit(1);
}

buf[numbytes] = '''';

printf(""Received: %s"",buf);

close(sockfd);

return 0;
}


注意,如果你在运行服务器之前运行客户程序,connect() 将返回 ""Connection refused"" 信息。


--------------------------------------------------------------------------------


数据报 Sockets
我不想讲更多了,所以我给出代码 talker.c 和 listener.c。

listener 在机器上等待在端口 4590 来的数据包。talker 发送数据包到一定的机器,他包含用户在命令行输入的东西。

这里就是 listener.c:

#include
#include
#include
#include
#include
#include
#include
#include

#define MYPORT 4950 /* the port users will be sending to */

#define MAXBUFLEN 100

main()
{
int sockfd;
struct sockaddr_in my_addr; /* my address information */
struct sockaddr_in their_addr; /* connector''s address information */
int addr_len, numbytes;
char buf[MAXBUFLEN];

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror(""socket"");
exit(1);
}

my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(MYPORT); /* short, network byte order */
my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */

if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))
== -1) {
perror(""bind"");
exit(1);
}

addr_len = sizeof(struct sockaddr);
if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0,
(struct sockaddr *)&their_addr, &addr_len)) == -1) {
perror(""recvfrom"");
exit(1);
}

printf(""got packet from %s "",inet_ntoa(their_addr.sin_addr));
printf(""packet is %d bytes long "",numbytes);
buf[numbytes] = '''';
printf(""packet contains ""%s"" "",buf);

close(sockfd);
}

注意在我们的调用 socket(),我们最后使用了 SOCK_DGRAM。同时,没有必要去使用 listen() 或者 accept()。我们在使用无连接的数据报套接口!

下面是 talker.c:

#include
#include
#include
#include
#include
#include
#include
#include
#include

#define MYPORT 4950 /* the port users will be sending to */

int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in their_addr; /* connector''s address information */
struct hostent *he;
int numbytes;

if (argc != 3) {
fprintf(stderr,""usage: talker hostname message "");
exit(1);
}

if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */
herror(""gethostbyname"");
exit(1);
}

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror(""socket"");
exit(1);
}

their_addr.sin_family = AF_INET; /* host byte order */
their_addr.sin_port = htons(MYPORT); /* short, network byte order */
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero), 8); /* zero the rest of the struct */

if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
(struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
perror(""sendto"");
exit(1);
}

printf(""sent %d bytes to %s "",numbytes,inet_ntoa(their_addr.sin_addr));

close(sockfd);

return 0;
}

这就是所有的了。在一台机器上运行 listener,然后在另外一台机器上运行 talker。观察他们的通讯!

Except for one more tiny detail that I''ve mentioned many times in the past: connected datagram sockets. I need to talk about this here, since we''re in the datagram section of the document. Let''s say that talker calls connect() and specifies the listener''s address. From that point on, talker may only sent to and receive from the address specified by connect(). For this reason, you don''t have to use sendto() and recvfrom(); you can simply use send() and recv().


--------------------------------------------------------------------------------


阻塞
阻塞,你也许早就听说了。""阻塞""是 ""sleep"" 的科技行话。你可能注意到前面运行的 listener 程序,他在那里不停地运行,等待数据包的到来。实际在运行的是他调用 recvfrom(),然后没有数据,因此 recvfrom() 说""阻塞 (block)"" 直到数据的到来。

很多函数都利用阻塞。accept() 阻塞,所有的 recv*() 函数阻塞。他们之所以能这样做是因为他们被允许这样做。当你第一次调用 socket() 建立套接口描述符的时候,内核就将他设置为阻塞。如果你不想套接口阻塞,你就要调用函数 fcntl():

#include
#include
.
.
sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
.
.


通过设置套接口为非阻塞,你能够有效地""询问""套接口以获得信息。如果你尝试着从一个非阻塞的套接口读信息并且没有任何数据,他不会变成阻塞--他将返回 -1 并将 errno 设置为 EWOULDBLOCK。

但是一般说来,这种轮询不是个好主意。如果你让你的程序在忙等状态查询套接口的数据,你将浪费大量的 CPU 时间。更好的解决之道是用下一章讲的 select() 去查询是否有数据要读进来。


--------------------------------------------------------------------------------


select()--多路同步 I/O
虽然这个函数有点奇怪,但是他很有用。假设这样的情况:你是个服务器,你一边在不停地从连接上读数据,一边在侦听连接上的信息。

没问题,你可能会说,不就是一个 accept() 和两个 recv() 吗? 这么容易吗,朋友? 如果你在调用 accept() 的时候阻塞呢? 你怎么能够同时接受 recv() 数据? ""用非阻塞的套接口啊!"" 不行!你不想耗尽所有的 CPU,不是吗? 那么,该如何是好?

select() 让你可以同时监视多个套接口。如果你想知道的话,那么他就会告诉你哪个套接口准备读,哪个又准备好了写,哪个套接口又发生了例外 (exception)。

闲话少说,下面是 select():

#include
#include
#include

int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

这个函数监视一系列文件描述符,特别是 readfds、writefds 和 exceptfds。如果你想知道你是否能够从标准输入和套接口描述符 sockfd 读入数据,你只要将文件描述符 0 和 sockfd 加入到集合 readfds 中。参数 numfds 应该等于最高的文件描述符的值加1。在这个例子中,你应该设置该值为 sockfd+1。因为他一定大于标准输入的文件描述符 (0)。

当函数 select() 返回的时候,readfds 的值修改为反映你选择的哪个文件描述符可以读。你可以用下面讲到的宏 FD_ISSET() 来测试。

在我们继续下去之前,让我来讲讲如何对这些集合进行操作。每个集合类型都是 fd_set。下面有一些宏来对这个类型进行操作:

FD_ZERO(fd_set *set) - clears a file descriptor set
FD_SET(int fd, fd_set *set) - adds fd to the set
FD_CLR(int fd, fd_set *set) - removes fd from the set
FD_ISSET(int fd, fd_set *set) - tests to see if fd is in the set

最后,是有点古怪的数据结构 struct timeval。有时你可不想永远等待别人发送数据过来。也许什么事情都没有发生的时候你也想每隔96秒在终端上打印字符串 ""Still Going...""。这个数据结构允许你设定一个时间,如果时间到了,而 select() 还没有找到一个准备好的文件描述符,他将返回让你继续处理。

数据结构 struct timeval 是这样的:

struct timeval {
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};

只要将 tv_sec 设置为你要等待的秒数,将 tv_usec 设置为你要等待的微秒数就可以了。是的,是微秒而不是毫秒。1,000微秒等于1豪秒,1,000毫秒等于1秒。也就是说,1秒等于1,000,000微秒。为什么用符号 ""usec"" 呢? 字母 ""u"" 很象希腊字母 Mu,而 Mu 表示 ""微"" 的意思。当然,函数返回的时候 timeout 可能是剩余的时间,之所以是可能,是因为他依赖于你的 Unix 操作系统。

哈!我们现在有一个微秒级的定时器!不要计算了,标准的 Unix 系统的时间片是100毫秒,所以无论你如何设置你的数据结构 struct timeval,你都要等待那么长的时间。

还有一些有趣的事情:如果你设置数据结构 struct timeval 中的数据为 0,select() 将立即超时,这样就可以有效地轮询集合中的所有的文件描述符。如果你将参数 timeout 赋值为 NULL,那么将永远不会发生超时,即一直等到第一个文件描述符就绪。最后,如果你不是很关心等待多长时间,那么就把他赋为 NULL 吧。

下面的代码演示了在标准输入上等待 2.5 秒:

#include
#include
#include

#define STDIN 0 /* file descriptor for standard input */

main()
{
struct timeval tv;
fd_set readfds;

tv.tv_sec = 2;
tv.tv_usec = 500000;

FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);

/* don''t care about writefds and exceptfds: */
select(STDIN+1, &readfds, NULL, NULL, &tv);

if (FD_ISSET(STDIN, &readfds))
printf(""A key was pressed! "");
else
printf(""Timed out. "");
}

如果你是在一个 line buffered 终端上,那么你敲的键应该是回车 (RETURN),否则无论如何他都会超时。

现在,你可能回认为这就是在数据报套接口上等待数据的方式--你是对的:他可能是。有些 Unix 系统可以按这种方式,而另外一些则不能。你在尝试以前可能要先看看本系统的 man page 了。

最后一件关于 select() 的事情:如果你有一个正在侦听 (listen()) 的套接口,你可以通过将该套接口的文件描述符加入到 readfds 集合中来看是否有新的连接。

这就是我关于函数 select() 要讲的所有的东西。


--------------------------------------------------------------------------------


More References
You''ve come this far, and now you''re screaming for more! Where else can you go to learn more about all this stuff?

Try the following man pages, for starters:

socket()
bind()
connect()
listen()
accept()
send()
recv()
sendto()
recvfrom()
close()
shutdown()
getpeername()
getsockname()
gethostbyname()
gethostbyaddr()
getprotobyname()
fcntl()
select()
perror()


Also, look up the following books:


Internetworking with TCP/IP, volumes I-III by Douglas E. Comer and David L. Stevens. Published by Prentice Hall. Second edition ISBNs: 0-13-468505-9, 0-13-472242-6, 0-13-474222-2. There is a third edition of this set which covers IPv6 and IP over ATM.


Using C on the UNIX System by David A. Curry. Published by O''Reilly & Associates, Inc. ISBN 0-937175-23-4.


TCP/IP Network Administration by Craig Hunt. Published by O''Reilly & Associates, Inc. ISBN 0-937175-82-X.


TCP/IP Illustrated, volumes 1-3 by W. Richard Stevens and Gary R. Wright. Published by Addison Wesley. ISBNs: 0-201-63346-9, 0-201-63354-X, 0-201-63495-3.

Unix Network Programming by W. Richard Stevens. Published by Prentice Hall. ISBN 0-13-949876-1.


On the web:


BSD Sockets: A Quick And Dirty Primer
(http://www.cs.umn.edu/~bentlema/unix/--has other great Unix system programming info, too!)

Client-Server Computing
(http://pandonia.canberra.edu.au/ClientServer/socket.html)

Intro to TCP/IP (gopher)
(gopher://gopher-chem.ucdavis.edu/11/Index/Internet_aw/Intro_the_Internet/intro.to.ip/)

Internet Protocol Frequently Asked Questions (France)
(http://web.cnam.fr/Network/TCP-IP/)

The Unix Socket FAQ
(http://www.ibrado.com/sock-faq/)



RFCs--the real dirt:


RFC-768 -- The User Datagram Protocol (UDP)
(ftp://nic.ddn.mil/rfc/rfc768.txt)

RFC-791 -- The Internet Protocol (IP)
(ftp://nic.ddn.mil/rfc/rfc791.txt)

RFC-793 -- The Transmission Control Protocol (TCP)
(ftp://nic.ddn.mil/rfc/rfc793.txt)

RFC-854 -- The Telnet Protocol
(ftp://nic.ddn.mil/rfc/rfc854.txt)

RFC-951 -- The Bootstrap Protocol (BOOTP)
(ftp://nic.ddn.mil/rfc/rfc951.txt)

RFC-1350 -- The Trivial File Transfer Protocol (TFTP)
(ftp://nic.ddn.mil/rfc/rfc1350.txt)



--------------------------------------------------------------------------------


Disclaimer and Call for Help
Well, that''s the lot of it. Hopefully at least some of the information contained within this document has been remotely accurate and I sincerely hope there aren''t any glaring errors. Well, sure, there always are.

So, if there are, that''s tough for you. I''m sorry if any inaccuracies contained herein have caused you any grief, but you just can''t hold me accountable. See, I don''t stand behind a single word of this document, legally speaking. This is my warning to you: the whole thing could be a load of crap.

But it''s probably not. After all, I''ve spent many many hours messing with this stuff, and implemented several TCP/IP network utilities for Windows (including Telnet) as summer work. I''m not the sockets god; I''m just some guy.

By the way, if anyone has any constructive (or destructive) criticism about this document, please send mail to [email protected] and I''ll try to make an effort to set the record straight.

In case you''re wondering why I did this, well, I did it for the money. Hah! No, really, I did it because a lot of people have asked me socket-related questions and when I tell them I''ve been thinking about putting together a socket page, they say, ""cool!"" Besides, I feel that all this hard-earned knowledge is going to waste if I can''t share it with others. WWW just happens to be the perfect vehicle. I encourage others to provide similar information whenever possible.

Enough of this--back to coding! ;-)


--------------------------------------------------------------------------------
Copyright © 1995, 1996 by Brian ""Beej"" Hall. This guide may be reprinted in any medium provided that its content is not altered, it is presented in its entirety, and this copyright notice remains intact. Contact [email protected] for more information.