下图是TCP客户端与服务器之间交互的一系列典型事件时间表:
为了执行网络I/O,一个进程(无论是服务端还是客户端)必须做的第一件事情就是调用socket
函数。
#include <sys/socket.h> /* basic socket definitions */
int socket(int family, int type, int protocol);/* 返回:非负描述字——成功,-1——出错 */
family
——协议族族 | 解释 |
---|---|
| IPv4协议 |
| IPv6协议 |
| Unix域协议 |
| 路由套接口 |
| 密钥套接口 |
type
——套接口类型类型 | 解释 |
---|---|
| 字节流套接口 |
| 数据报套接口 |
| 原始套接口 |
下面是有效的family
和type
组合(简略版):
|
| |
---|---|---|
| TCP | TCP |
| UDP | UDP |
| IPv4 | IPv6 |
socket
函数返回一个套接口描述字,简称套接字(sockfd
)。获取套接字无需指定地址,只需要指定协议族和套接口类型(如上表中的组合)。
TCP客户用connect
函数来建立一个与TCP服务器的连接。
#include <sys/socket.h> /* basic socket definitions */
int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/* 返回:0——成功,-1——出错 */
sockfd
便是socket
函数返回的套接口描述字。servaddr
必须包含服务器的IP地址和端口号。bind
函数),内核会选择源IP和一个临时端口。connect
函数会触发TCP三次握手。有可能出现下面的错误情况:1.客户端未收到SYN
分节的响应
第一次发出未收到,间隔6s再发一次,再没收到,隔24秒再发一次,总共等待75s还没收到则返回错误( ETIMEDOUT
)。可以用时间日期程序验证一下:
查看本地网络信息:
JACKIELUO-MC0:intro jackieluo$ ifconfig
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
ether f4:0f:24:2a:72:a6
inet6 fe80::1830:dbd:1b29:2989%en0 prefixlen 64 secured scopeid 0x6
inet 192.168.0.101 netmask 0xffffff00 broadcast 192.168.0.255
nd6 options=201<PERFORMNUD,DAD>
media: autoselect
status: active
将程序指向本地地址192.168.0.101
(确保时间日期服务器程序已运行),成功:
JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101
Sat Oct 6 17:06:55 2018
将程序指向本地子网地址192.168.0.102
,其主机ID(102)不存在,等待几分钟后超时返回:
JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.102
connect error: Operation timed out
2.收到RST
即服务器主机在指定端口上没有等待连接的进程,这称为“hard error”,客户端一接收到RST
,马上返回错误(ECONNREFUSED
)。验证:
关闭之前本机运行的daytimetcpsrv
进程
将程序指向本地地址192.168.0.101
:
JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101
connect error: Connection refused
3.发出的SYN
在路由器上引发了目的不可达ICMP
错误
这个错误被称为“soft error”,最终返回EHOSTUNREACH
或者ENETUNREACH
。
函数bind
为套接口分配一个本地协议地址,包括IP地址和端口号。
#include <sys/socket.h> /* basic socket definitions */
int bind(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/* 返回:0——成功,-1——出错 */
bind
函数绑定ip地址和端口,供客户端调用。一个例外是RPC(远程过程调用)服务器,它由内核为其选择临时端口。然后通过RPC端口映射器进行注册,客户端与该服务器连接之前,先通过端口映射器获取服务器的端口。SYN
所在分组的目的IP地址作为服务器的源IP地址。(即服务器收到SYN
的IP)给函数bind
指定用于捆绑的IP地址和/或端口号的结果:
IP地址 | 端口 | 结果 |
---|---|---|
0 | 内核选择IP地址和端口 | |
非0 | 内核选择IP地址,进程指定端口 | |
本地IP地址 | 0 | 进程选择IP地址,内核指定端口 |
本地IP地址 | 非0 | 进程选择IP地址和端口 |
函数listen
仅被TCP服务器调用。
#include <sys/socket.h> /* basic socket definitions */
int listen(int sockfd, int backlog);/* 返回:0——成功,-1——出错 */
调用函数socket
函数创建的套接口,默认是主动方,下一步应是调用connect
,CLOSED
的下一个状态是SYN_SENT
(见TCP状态转换图)。而函数listen
将套接口转换成被动方,告诉内核,应接受指向此套接口的连接请求,CLOSED
状态变成LISTEN
。
函数listen
的第二个参数backlog
表示内核为此套接口排队的最大连接数。对于给定的监听套接口,内核会维护两个队列:
SYN_RCVD
状态。ESTABLISHED
状态。accept
函数时,已完成连接队列的头部条目返回给进程。backlog
函数accept
由TCP服务器调用,从已完成连接队列头部返回下一个已完成连接,若该队列为空,则进程睡眠(假定套接口为默认的阻塞方式)。
#include <sys/socket.h> /* basic socket definitions */
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);/* 返回:非负描述字——成功,-1——出错 */
函数accept
的第一个参数和返回值都是套接口描述字。其中,
socket
返回,也用于bind
,listen
的第一个参数。通常一个服务器,只生成一个监听套接口描述字,直到其关闭。而内核为每个被接受的客户连接,创建一个已连接套接口,当客户连接完成时,关闭该已连接套接口。
注意到intro/daytimetcpsrv.c
中,后两个参数传的都是空指针,这是因为我们不关注客户的身份,无需知道客户的协议地址。
connfd = Accept(listenfd, (SA *) NULL, NULL);
稍作修改,不再传入空指针,见intro/daytimetcpsrv1.c
:
socklen_t len;
struct sockaddr_in servaddr, cliaddr;
...
connfd = Accept(listenfd, (SA *) &cliaddr, &len);
printf("connection from %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
ntohs(cliaddr.sin_port));
kill掉之前的daytimetcpsrv
进程:
$ sudo lsof -i -P | grep -i "listen"
daytimetc 80986 root 3u IPv4 0xae12d925e4528793 0t0 TCP *:13 (LISTEN)
$ sudo kill -9 80986
编译运行新的服务端程序:
$ make daytimetcpsrv1.c daytimetcpsrv1
$ ./daytimetcpsrv1
重复执行客户端程序,发几个请求:
$ ./daytimetcpcli 127.0.0.1
Wed Sep 26 14:11:20 2018
$ ./daytimetcpcli 127.0.0.1
Wed Sep 26 14:17:06 2018
查看服务端打印:
connection from 127.0.0.1, port 58201
connection from 127.0.0.1, port 58342
注意到,由于客户端程序没有调用bind
函数,内核为它的协议地址选择了源ip作为IP地址,临时端口号也发生了变化。
#include <unistd.h>
pid_t fork(void);/* 返回:在子进程中为0,在父进程中为子进程ID,-1——出错 */
fork
函数调用一次,却返回两次。
getppid
来得到父进程的ID通过返回值可以判断当前进程是子进程还是父进程。
父进程在调用fork
之前打开的所有描述字在函数fork
返回后都是共享的。网络服务器会利用这一特性:
accept
。fork
,已连接套接口就在父进程与子进程间共享。(一般来说就是子进程读、写已连接套接口,而父进程关闭已连接套接口)。fork
有两个典型应用:
fork
生成一个拷贝,利用子进程调用exec
来执行新的程序。典型应用是shell。以文件形式存储在硬盘上的可执行程序若要被执行,需要由一个现有进程调用exec
函数。我们将调用exec
的进程称为调用进程,新程序的进程ID并不改变,仍处于当前进程。
客户和服务器,从调用socket
开始,返回一个套接口描述字。客户调用connect
,服务器调用bind
、listen
、accept
。最后套接口由close
关闭。
多数TCP服务器是调用fork
来实现并发处理多客户请求的。多数UDP服务器则是迭代的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。