UNIX网络编程 - Chapter 4 基本TCP套接字编程

Chapter 4 基本TCP套接字编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include "unp.h"

int main() {
pid_t pid; // 进程号
int listenfd, connfd; // 监听标识符和连接标识符
struct sockaddr_in servaddr, cliaddr; // servaddr表示服务器的监听地址和端口, cliaddr是客户端连接的地址
char buff[MAXLINE]; // 缓冲区
socklen_t len; // 值-结果参数(value-result)

// 创建TCP套接字
listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 指定期望的通信协议类型

// 初始化服务器地址
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听任意IP地址, 因为代码在主机上运行, 所以这里要将主机序转为网络序
servaddr.sin_port = htons(13);

// 将服务器地址绑定到监听套接字
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); // 第二个参数要进行强制类型转换, 为了通用接口设计, 详见上一节

// 将套接字转换为监听状态
Listen(listenfd, LISTENQ); // 将未连接的套接字转换成一个被动套接字, 指示内核应接受指向该套接字的连接请求, 使CLOSED状态转换为LISTEN状态

while(1) {
len = sizeof(cliaddr); // 初始化客户端地址大小(值-结果参数)

connfd = Accept(listenfd, (SA *) &cliaddr, &len); // 从已完成连接队列中返回下一个已完成连接, 同时修改len为实际的cliaddr大小

/**
* 并发处理
* Fork()父进程返回新建子进程的id, 子进程返回0
* Fork()之后子进程和父进程在逻辑上是同时进行的, 实际由OS调度算法决定(比如 时间片轮转)
* */
if((pid = Fork() == 0)) { // 如果返回0说明现在是子进程
// 【子进程专区】
// 只有 fork 返回 0 的进程(子进程)才会进入这里
// 子进程的任务:处理刚才建立的连接 (connfd)
Close(listenfd); // 关闭监听端口
doit(connfd); // 逻辑处理
Close(connfd); // 处理结果关闭连接
exit(0); // 退出
}

// 【父进程专区】
// 只有 fork 返回正整数的进程(父进程)才会跳过上面的 if
// 父进程的任务:继续监听,生更多的孩子
Close(connfd); // 父进程把连接交给孩子了,自己手里的这个可以关了
}

}

Q&A

问题 4.1:在 4.4 节中,我们说头文件<netinet/in.h>中定义的INADDR_*常值是主机字节序的。我们应该如何辨别?

网络字节序为大端序, 而主机字节序分为大端序和小端序, 判断大端序和小端序的方法最简单的为:

定义一个整数1( 0x00000001), 然后用主机读取第一个字节(起始地址), 如果为1那么就说明为小端序

1
2
3
4
5
6
7
int x = 1;
char *p = (char *)&x;

if(*p == 1)
printf("小端序\n");
else
printf("大端序\n");

问题 4.2:把图 1-5 改为在connect成功返回后调用getsockname。使用sock_ntop显示赋予 TCP 套接字的本地 IP 地址和本地端口号。你的系统的临时端口号在什么范围内(图 2-10)?

1
2
3
4
5
6
7
8
9
10
11
socklen_t			len;
struct sockaddr_storage localaddr;

...

len = sizeof(localaddr);
if(getsockname(sockfd, (SA *) &localaddr, &len) < 0)
return -1;
else{
printf("TCP ip address is %s\n", sock_ntop((SA *) &localaddr, len));
}

如何查看临时端口确切的范围

1
cat /proc/sys/net/ipv4/ip_local_port_range

或者

1
sysctl net.ipv4.ip_local_port_range

问题 4.3:在一个并发服务器中,假设fork调用返回后子进程先运行,而且子进程随后在fork调用返回父进程之前就完成对客户的服务。图 4-13 中的两个close调用将会发生什么?

详见上面的程序

问题 4.4:在图 4-11 中,先把服务器的端口号从 13 改为 9999(这样不需要超级用户特权就能启动程序),再删掉listen调用,将会发生什么?

会出现下面的情况:

1
2
root@eec97ceac465:/workspaces/unp/unpv13e/intro# ./daytimetcpsrv1
accept error: Invalid argument

原因如下:

TCP连接的三个步骤:

  1. Socket() - 创建套接字, socket()创建套接字时, 默认为一个主动套接字, 即是一个将调用connect连接的客户套接字
  2. Bind() - 绑定地址和端口
  3. Listen() - 将套接字转换为被动/监听状态, 此时套接字从CLOSED状态转换到LISTEN状态

所以当Listen()被注释时, 套接字仍处于CLOSED状态, Accept()无法在非监听套接字上工作, 所以报”Invalid argument”错误

即:

  • Listen()必须在Bind()之后调用
  • Accept()只能在套接字处于 LISTEN 状态时使用
  • 没有 Listen() → 套接字不能接受连接 → Accept() 报 “Invalid argument”