# 8. 常见的问题

**我可以从哪边取得那些 header 文件呢?**\
\
如果你的系统还没有这些文件，你可能就不需要它们。检查你平台的手册。若你在 Windows 上开发，那麽你只需要 #include \<winsock.h>。\
\
**在 bind() 回报＂Address already in use＂（地址已经在使用中）时，我该怎麽办呢？**\
\
你必须使用 setsockopt() 对 listen 的 socket 设置 SO\_REUSEADDR 选项。请参考 bind() 及 select() 章节的示例。\
\
**我该如何取得系统上已经开启的 sockets 表呢？**\
\
使用 **netstat**。细节请参考 man 手册，不过你应该只要输入下列的命令就能取得一些不错的资料：<br>

```
$ netstat
```

\
**我该如何检视 routing table（路由表）呢？**\
\
使用 **route** 命令（多数的 Linux 系统是在 /sbin 底下），或者 **netstat -r** 命令。\
\
**如果我只有一台电脑，我该如何运行 client（客户）与 server（服务器）程序呢？我需要网路来做网路编程吗？**\
\
你很幸运，全部的系统都有实现一个 loopback 虚拟网路 ＂设备＂（device），这个设备位於 kernel（内核），并假装是个网卡［这个接口就是 routing table 中所列出的 ＂lo＂］。\
\
假装你已经登录一个名为＂goat＂的系统，在一个窗口运行 client，并在另一个窗口运行 server。\
\
或者可以在背景运行 server［＂server &＂］，并在同样的窗口运行 client。\
\
loopback 设备的功能是你可以运行 **client goat** 或 **client localhost**［因为＂localhost＂应该已经定义在你的 /etc/hosts 文件］，而你可以让 client 与 server 通讯而不需要网路。\
\
简而言之，不需要改变任何代码，就可以让程序在无网路的本地端系统上运行！好耶！\
\
**我该怎麽识别对方已经关闭连接呢？**\
\
你可以辨别出来，因为 recv() 会返回 0。\
\
**我该如何实现一个＂ping＂工具呢？什麽是 ICMP 呢？我可以在哪里找到更多关於 raw socket 与 SOCK\_RAW 的资料呢？**\
\
你对 raw socket 的全部疑问都可以在 W. Richard Stevens 的 UNIX Network Programming 书本上找到答案。还有，研究 Stevens 的 UNIX Network Programming 代码的 ping 子目录，可以在线下载 \[37]。\
\
**我该如何改变或减少调用 connect() 的 timeout（超时）时间呢？**\
\
我不想跟你说一样的答案：＂W. Richard Stevens 会告诉你＂，我只能建议你阅读 UNIX Network Programming 代码 \[38] 中的 /lib/connect\_nonb.c。\
\
主要是你要用 socket() 建立一个 socket descriptor，将它设置为 non-blocking（非阻塞），调用 **connect()**，而如果一切顺利，**connect()**&#x4F1A;立馬返回 -1，并将 errno 设置为 EINPROGRESS。接着你要调用 **select()** 并设置你想要的 timeout 时间，传递读写组（read and write sets）的 socket descriptor。如果 select() 没有发生 timeout，这表示 connect() call 已经完成。此时，你必须使用 **getsockopt()** 设置 SO\_ERROR 选择项以取得 connect() call 的返回值，在没有错误时，这个值应该是零。\
\
最後，在你开始透过 socket 传输数据以前，你可能想要再将它设置回 blocking（阻塞）。\
\
要注意的是，这麽做的好处是让你的程序在连接（connecting）期间也可以另外做点事情。比如：你可以将 timeout 时间设定为类似 500 毫秒，并在每次 timeout 发生时更新显示器画面，然後再次调用 select()。当你已经调用了 select() 时，并且 timeout 了，像这样重复了 20 次，你就会知道应该放弃这个连接了。\
\
如我所述的，请参考 Stevens 的既完美又优秀的代码示例。\
\
**我该如何写 Windows 的网路程序呢？**\
\
首先，请删除 Windows，并安装 Linux 或 BSD。;-)。不是的，实际上，只要参考前言中的［Windows 程序设计师要注意的事情］就可以了。\
\
**我该如何在 Solaris/SunOS 上编译程序呢？在我尝试编译时，一直遇到错误！**\
\
发生 Linker（链接器）错误是因为 Sun 系统在不会自动编入 socket 程序库。请参考前言中的［Solaris/SunOS 程序员要注意的事情］，有如何处理这个问题的示例。\
\
**为什麽 select() 一直跟 signal 吵架呢？**\
\
Signal 试图要让 blocked system call 返回 -1，并将 errno 设置为 EINTR。当你用 **sigaction()** 设置了一个 signal handler（信号处理例程）时，你可以设置 SA\_RESTART flag，这可以在 system call 被中断之後重新打开它。\
\
很自然的是这不会每次都管用。\
\
我最爱的解法是使用一个 goto，你明白这会让你的教授很愤怒，所以放手去做吧！<br>

<pre class="language-c"><code class="lang-c">select_restart:
if ((err = select(fdmax+1, &#x26;readfds, NULL, NULL, NULL)) == -1) {
    if (errno == EINTR) {
<strong>        // 某个 signal 中断了我们，所以重新启动
</strong>        goto select_restart;
    }
    // 这里处理真正的错误：
    perror("select");
}
</code></pre>

当然，在这个例子里，你不需使用 goto；你可以用其它的 structures 来控制，但是我认为用 goto 比较乾净。\
\
**要怎麽样我才能实作调用 recv() 的 timeout 呢？**\
\
使用 **select()**！它可以让你对正在读取的 socket descriptors 指定 timeout 的参数。或者你可以将整个功能包在一个独立的函数中，类似这样：<br>

```c
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>

int recvtimeout(int s, char *buf, int len, int timeout)
{
  fd_set fds;
  int n;
  struct timeval tv;

  // 设置 file descriptor set
  FD_ZERO(&fds);
  FD_SET(s, &fds);

  // 设置 timeout 的数据结构 struct timeval
  tv.tv_sec = timeout;
  tv.tv_usec = 0;

  // 一直等到 timeout 或收到数据
  n = select(s+1, &fds, NULL, NULL, &tv);
  if (n == 0) return -2; // timeout!
  if (n == -1) return -1; // error

  // 数据一定有在这里，所以调用一般的 recv()
  return recv(s, buf, len, 0);
}
.
.
.
// 调用 recvtimeout() 的示例：
n = recvtimeout(s, buf, sizeof buf, 10); // 10 second timeout

if (n == -1) {
  // 发生错误
  perror("recvtimeout");
}
else if (n == -2) {
  // 发生 timeout
} else {
  // 从 buf 收到一些数据
}
.
.
.
```

请注意，**recvtimeout()** 在 timeout 的例子中会返回 -2，那为什麽不是返回 0 呢？好的，如果你还记得，在调用 recv() 返回 0 值时所代表的意思是对方已经关闭了连接。所以该返回值已经用过了，而 -1 表示＂错误＂，所以我选择 -2 做为我的 timeout 表示。\
\
**我该如何在将数据送给 socket 以前将数据加密或压缩呢？**\
\
一个简单的加密方法是使用 SSL（secure sockets layer），只是这超过本教程的范畴了［细节请参考 OpenSSL project \[39]］。\
\
不过假设你想要安插或实现你自己的压缩器（compressor）或加密系统（encryption system），这只不过是将你的数据想成在两个节点间运行连续的步骤，每个步骤以同样的方式改变数据：\
\
1\. server 从文件读取数据［或是什麽地方］\
\
2\. server 加密/压缩数据［你新增这个部分］\
\
3\. server 用 send() 送出加密数据\
\
而另一边则是：\
\
1\. client 用 recv() 接收加密数据\
\
2\. client 译码/解压数据［你新增这个部分］\
\
3\. client 写数据到文件［或是什麽地方］\
\
如果你正要压缩与加密，只要记得先压缩。:-)\
\
只要 client 适当地还原 server 所做的事情，数据在另一端就会完好如初，不论你在中间增加了多少步骤。\
\
所以你用我的代码所需要做的只有：找出读数据与透过网路传送［使用 **send()**］这中间的段落，并在那里加上编码的代码。\
\
**我一直看到的 ＂PF\_INET＂是什麽呢？他跟 AF\_INET 有关系吗？**\
\
是的，有关系，细节请参考 socket() 章节。\
\
**我该怎麽写一个 server，可以接受来自 client 的 shell 命令并运行命令呢？**\
\
为了简化，我们说 client 的连接用 connect()丶send() 以及 close()［即为，没有後续的 system calls，client 没有再次连接。］\
\
client 的处理过程是：\
\
1\. 用 connect() 连接到 server\
\
2\. send("/sbin/ls > /tmp/client.out")\
\
3\. 用 close() 关闭连接\
\
此时，server 正在处理数据并运行命令：\
\
1\. accept() client 的连接\
\
2\. 使用 recv(str) 接收命令字符串\
\
3\. 用 close() 关闭连接\
\
4\. 用 system(str) 运行命令\
\
注意！server 会运行全部 client 所送的命令，就像是提供了远端的 shell 权限，人们可以连接到你的 server 并用你的帐户做点事情。例如：若 client 送出 ＂rm -rf \~＂会怎麽样呢？这会删掉你帐户里全部的数据，就是这样！\
\
所以你学聪明了，你会避免 client 使用任何危险的工具，比如 **foobar** 工具：<br>

```c
if (!strncmp(str, "foobar", 6)) {
    sprintf(sysstr, "%s > /tmp/server.out", str);
    system(sysstr);
}
```

可是你还是不安全，没错：如果 client 输入 ＂**foobar; rm -rf \~**＂呢？\
\
最安全的方式是写一个小机制，将命令参数中的非字母数字字符前面放个［＂\＂］字符［如果适合的话，要包括空白］。\
\
如你所见，当 server 开始运行 client 送来的东西时，安全（security）是个问题。\
\
**我正在传送大量数据，可是当我 recv() 时，它一次只收到 536 bytes 或 1460 bytes。可是如果我在我本地端运行，它就会一次就收到全部的数据，这是怎麽回事呢？**\
\
你碰到的是 MTU，即 physical medium（物理媒体）能处理的最大尺寸。在本地端上，你用的是 loopback 设备，它可以处理 8K 或更多数据也没有问题。但是在 Ethernet（以太网），它只能处理 1500 bytes［有 header］，你碰到这个限制。透过 modem 的话，MTU 是 576 bytes［一样，有 header］，你遇到比较低的限制。\
\
你必须确认有送出全部的数据。［细节请参考 **sendall()** 函数的实务］。一旦你有确认，那麽你就需要在循环中调用 recv()，直到收到全部的数据。\
\
对於使用多重调用 **recv()** 来接收完整数据包的细节，请参考数据封装之子（Son of Data Encapsulation）一节。\
\
**我用的是 Windows 系统，而且我没有 fork() system call 或任何的 struct sigaction 可以用，该怎麽办呢？**\
\
如果你问的是它们在哪里，它们会在 POSIX 程序库里，这个会包装在你的编译器中。因为我没有 Windows 系统，所以我真的无法回答你，不过我似乎记得 Microsoft 有一个 POSIX 兼容层，那里会有 **fork()**。［而且甚至会有 sigaction。］\
\
在 VC++ 的手册搜寻 ＂fork＂ 或 ＂POSIX＂，看它是否能给你什麽线索。\
\
如果这样一点都没有用，拿掉 fork()/sigaction 这些东西，用 Win32 中等价的函数来替换：**CreateProcess()**。我不知道怎麽用**CreateProcess()**，它有多的数不清的参数，不过在 VC++ 的资料中应该可以找到怎麽使用它。\
\
**我在防火墙（firewall）後面，我该如何让防火墙外面的人知道我的 IP 地址，让他们可以连接到我的电脑呢？**\
\
毫无疑问地，防火墙的目的就是要防止防火墙外面的人连到防火墙里面的电脑，所以你让他们进来基本上会被认为是安全漏洞。\
\
但也不是说完全不行，有一个方法，你仍然可以透过防火墙频繁的进行 **connect()**，如果防火墙是使用某种伪装（masquerading）或 NAT 或类似的方式。你只要让程序一直在做初始化连接，那麽你有机会成功的。\
\
如果这样还不是很满意，你可以要求系统管理员在防火墙开一个小洞（hole），让人们可以连进你的电脑。防火墙可以透过 NAT 软件或 proxy（代理）或类似的方法将数据包转送给你。\
\
要留意，不要对防火墙中的一个小洞掉以轻心。你必须确保你不会放坏人进来存取内网；如果你是新手，做软件安全的难度是远远超过你的想像。\
\
不要让你的系统管理员对我发脾气 ;-)\
\
**我该怎麽写 packet sniffer 呢？我要怎麽将我的 Ethernet interface 设置为 promiscuous mode（混杂模式）呢？**\
\
这些事情是在底层运作的，当网卡设置为＂promiscuous mode＂时，它会转送全部的数据给操作系统，而不只是地址属於这台电脑的数据包而已。［我们这里谈的是 Ethernet 层的地址，而不是 IP 地址，可是因为 ethernet 是在 IP 底层，所以全部的 IP 地址实际上都会转送。细节请参考＂底层漫谈与网路理论＂一节］。\
\
这是 packet sniffer 如何运作的基础，它将网卡设置为 promiscuous mode，接着 OS 会收到经过网线的每个数据，你会有一个可以用来读取数据的某种型别 socket。\
\
毫无疑问地，这个问题的答案依平台而异，不过如果你用百度或 Google 搜寻，例如：＂windows promiscuous ioctl＂，你或许会在某个地方找到，看起来跟 Linux Journal \[40] 中写的一样好的。\
\
**我该如何为 TCP 或 UDP socket 设定一个自订的 timeout 值呢？**\
\
这个按照你的系统而定，你可以在网上搜寻 SO\_RCVTIMEO 与 SO\_SNDTIMEO［用在 **setsockopt()**］，看看是否你的系统有支持这样的功能。\
\
Linux man 手册建议使用 **alarm()** 或 **setitimer()** 作为替代品。\
\
**我要如何辨别哪些 ports 可以使用呢？有没有＂官方＂的 port numbers 呢？**\
\
通常这不会有问题，如果你正在写像 web server 这样的程序，那麽在你的程序使用 port 80 是个好主意。如果你只是想要写自己的 server，那麽随机选择一个 port［不过要大於 1023］，然後试试看。\
\
如果 port 已经在使用中，你将会在尝试 **bind()** 时遇到＂Address already in use＂错误。选择另一个 port。［利用 config 配置文件或命令行参数，让你的软件用户能指定 port 也是个不错的想法］。\
\
有一个官方的 port nubmer \[41] 表，由 Internet Assigned Numbers Authority（IANA）所维护的。在表中的 ports［超过 1023］并不代表你不能使用，比如，Id 软件的 DOOM 跟 ＂mdqs＂ 用一样的 port，不管那是什麽，最重要的是*在同一台机器上*没有人用掉你要用的 port。\
\
\[37] <http://www.unpbook.com/src.html\\>
\[38] <http://www.unpbook.com/src.html\\>
\[39] <http://www.openssl.org/\\>
\[40] <http://interactive.linuxjournal.com/article/4659\\>
\[41] <http://www.iana.org/assignments/port-numbers>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://beej-zhcn.netdpi.net/faq.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
