值得收藏的TCP套接口编程文章

qcloudcommunity · · 2960 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

**欢迎大家前往[腾讯云+社区](https://cloud.tencent.com/developer/?fromSource=waitui),获取更多腾讯海量技术实践干货哦~** > 本文由[jackieluo](https://cloud.tencent.com/developer/user/1205848?fromSource=waitui)发表于[云+社区专栏](https://cloud.tencent.com/developer/column/4514?fromSource=waitui) ## TCP客户端-服务器典型事件 下图是TCP客户端与服务器之间交互的一系列典型事件时间表: 1. 首先启动服务器,等待客户端连接 2. 启动客户端,连接到服务器 3. 客户端发送一个请求给服务器,服务器处理请求,响应客户端 4. 循环步骤3 5. 客户端给服务器发一个文件结束符,关闭客户端连接 6. 服务器也关闭连接 ![img](https://ask.qcloudimg.com/draft/1205848/k2dav9qf7d.png?imageView2/2/w/1620)基本TCP客户-服务器程序的套接口函数 ## 套接口编程基本函数 ### socket 函数 为了执行网络I/O,一个进程(无论是服务端还是客户端)必须做的第一件事情就是调用`socket`函数。 ```js #include <sys/socket.h> /* basic socket definitions */ int socket(int family, int type, int protocol);/* 返回:非负描述字——成功,-1——出错 */ ``` - `family`——协议族 | 族 | 解释 | | ---------- | ---------- | | `AF_INET` | IPv4协议 | | `AF_INET6` | IPv6协议 | | `AF_LOCAL` | Unix域协议 | | `AF_ROUTE` | 路由套接口 | | `AF_KEY` | 密钥套接口 | - `type`——套接口类型 | 类型 | 解释 | | ------------- | ------------ | | `SOCK_STREAM` | 字节流套接口 | | `SOCK_DGRAM` | 数据报套接口 | | `SOCK_RAW` | 原始套接口 | 下面是有效的`family`和`type`组合(简略版): | | `AF_INET` | `AF_INET6` | | ------------- | --------- | ---------- | | `SOCK_STREAM` | TCP | TCP | | `SOCK_DGRAM` | UDP | UDP | | `SOCK_RAW` | IPv4 | IPv6 | `socket`函数返回一个套接口描述字,简称套接字(`sockfd`)。获取套接字无需指定地址,只需要指定协议族和套接口类型(如上表中的组合)。 ### connect函数 TCP客户用`connect`函数来建立一个与TCP服务器的连接。 ```js #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`)。可以用时间日期程序验证一下: 查看本地网络信息: ```js 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`(确保时间日期服务器程序已运行),成功: ```js JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101 Sat Oct 6 17:06:55 2018 ``` 将程序指向本地子网地址`192.168.0.102`,其主机ID(102)不存在,等待几分钟后超时返回: ```js 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`: ```js JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101 connect error: Connection refused ``` 3.发出的`SYN`在路由器上引发了目的不可达`ICMP`错误 这个错误被称为“soft error”,最终返回`EHOSTUNREACH`或者`ENETUNREACH`。 ### bind函数 函数`bind`为套接口分配一个本地协议地址,包括IP地址和端口号。 ```js #include <sys/socket.h> /* basic socket definitions */ int bind(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/* 返回:0——成功,-1——出错 */ ``` - 客户端可以不调用这个函数,由内核选择一个本地ip的临时端口就好。 - 服务器一般都会调用`bind`函数绑定ip地址和端口,供客户端调用。一个例外是RPC(远程过程调用)服务器,它由内核为其选择临时端口。然后通过RPC端口映射器进行注册,客户端与该服务器连接之前,先通过端口映射器获取服务器的端口。 - 进程可以把一个特定的IP地址捆绑到它的套接口上。对于客户端,它发送的请求,源IP地址就是这个地址;对于服务器,如果绑定了IP地址,则只接受目的地为此IP地址的客户连接。 - 如果服务器不把IP地址绑定到套接口上,那么内核把客户端发送`SYN`所在分组的目的IP地址作为服务器的源IP地址。(即服务器收到`SYN`的IP) 给函数`bind`指定用于捆绑的IP地址和/或端口号的结果: | IP地址 | 端口 | 结果 | | ---------- | ---- | ---------------------------- | | | 0 | 内核选择IP地址和端口 | | | 非0 | 内核选择IP地址,进程指定端口 | | 本地IP地址 | 0 | 进程选择IP地址,内核指定端口 | | 本地IP地址 | 非0 | 进程选择IP地址和端口 | ### listen函数 函数`listen`仅被TCP服务器调用。 ```js #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`表示内核为此套接口排队的最大连接数。对于给定的监听套接口,内核会维护两个队列: 1. 未完成连接队列(incomplete connection queue) SYN分节已由客户发出,到达服务器,正在进行TCP的三路握手。此时这些套接口处于`SYN_RCVD`状态。 2. 已完成连接队列(completed connection queue) SYN分节已由客户发出,到达服务器,并且已完成三路握手。此时这些套接口处于`ESTABLISHED`状态。 3. 当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新条目,直到三路握手中,第三个分节(客户对服务SYN的ACK)到达,这个条目移到已完成连接队列的队尾。 4. 当进程调用`accept`函数时,已完成连接队列的头部条目返回给进程。 5. 两个队列之和不能超过`backlog` 6. 当一个客户SYN到达时,若这两个队列都是满的,TCP就忽略此分节,且不发送RST。客户TCP将重发SYN,期望不久就能在队列中找到空闲位置。 ![img](https://ask.qcloudimg.com/draft/1205848/3zveblq98i.png?imageView2/2/w/1620)TCP为监听套接口维护的两个队列 ### accept函数 函数`accept`由TCP服务器调用,从已完成连接队列头部返回下一个已完成连接,若该队列为空,则进程睡眠(假定套接口为默认的阻塞方式)。 ```js #include <sys/socket.h> /* basic socket definitions */ int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);/* 返回:非负描述字——成功,-1——出错 */ ``` 函数`accept`的第一个参数和返回值都是套接口描述字。其中, 1. 第一个参数,称为监听套接口描述字,即由函数`socket`返回,也用于`bind`,`listen`的第一个参数。 2. 返回值,称为已连接套接口描述字。 通常一个服务器,只生成一个监听套接口描述字,直到其关闭。而内核为每个被接受的客户连接,创建一个已连接套接口,当客户连接完成时,关闭该已连接套接口。 注意到`intro/daytimetcpsrv.c`中,后两个参数传的都是空指针,这是因为我们不关注客户的身份,无需知道客户的协议地址。 ```js connfd = Accept(listenfd, (SA *) NULL, NULL); ``` 稍作修改,不再传入空指针,见`intro/daytimetcpsrv1.c`: ```js 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`进程: ```js $ sudo lsof -i -P | grep -i "listen" daytimetc 80986 root 3u IPv4 0xae12d925e4528793 0t0 TCP *:13 (LISTEN) $ sudo kill -9 80986 ``` 编译运行新的服务端程序: ```js $ make daytimetcpsrv1.c daytimetcpsrv1 $ ./daytimetcpsrv1 ``` 重复执行客户端程序,发几个请求: ```js $ ./daytimetcpcli 127.0.0.1 Wed Sep 26 14:11:20 2018 $ ./daytimetcpcli 127.0.0.1 Wed Sep 26 14:17:06 2018 ``` 查看服务端打印: ```js connection from 127.0.0.1, port 58201 connection from 127.0.0.1, port 58342 ``` 注意到,由于客户端程序没有调用`bind`函数,内核为它的协议地址选择了源ip作为IP地址,临时端口号也发生了变化。 ### fork和exec函数 ```js #include <unistd.h> pid_t fork(void);/* 返回:在子进程中为0,在父进程中为子进程ID,-1——出错 */ ``` `fork`函数调用一次,却返回两次。 1. 在调用它的进程(即父进程),它返回一次,返回值是派生出来的子进程的进程ID。 父进程可能有很多子进程,必须通过返回值跟踪记录子进程ID。 2. 在子进程,它还返回一次,返回值为0。 子进程只有一个父进程,总可以通过`getppid`来得到父进程的ID 通过返回值可以判断当前进程是子进程还是父进程。 父进程在调用`fork`之前打开的所有描述字在函数`fork`返回后都是共享的。网络服务器会利用这一特性: 1. 父进程调用`accept`。 2. 父进程调用`fork`,已连接套接口就在父进程与子进程间共享。(一般来说就是子进程读、写已连接套接口,而父进程关闭已连接套接口)。 `fork`有两个典型应用: 1. 一个进程为自己派生一个拷贝,并发执行任务,这也是典型的并发网络服务器模型。 2. 一个进程想执行其他的程序,于是调用`fork`生成一个拷贝,利用子进程调用`exec`来执行新的程序。典型应用是shell。 以文件形式存储在硬盘上的可执行程序若要被执行,需要由一个现有进程调用`exec`函数。我们将调用`exec`的进程称为调用进程,新程序的进程ID并不改变,仍处于当前进程。 ## 小结 客户和服务器,从调用`socket`开始,返回一个套接口描述字。客户调用`connect`,服务器调用`bind`、`listen`、`accept`。最后套接口由`close`关闭。 多数TCP服务器是调用`fork`来实现并发处理多客户请求的。多数UDP服务器则是迭代的。 > **相关阅读** > [系统重启后nginx reload不生效原因分析](https://cloud.tencent.com/developer/article/1339917?fromSource=waitui) > [SRS开源直播服务 - StateThreads微线程框架学习](https://cloud.tencent.com/developer/article/1197338?fromSource=waitui) > [高性能网络编程3----TCP消息的接收](https://cloud.tencent.com/developer/article/1345058?fromSource=waitui) > [【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识](https://cloud.tencent.com/developer/edu/course-1128?fromSource=waitui) **此文已由作者授权腾讯云+社区发布,更多原文请[点击](https://cloud.tencent.com/developer/article/1351071?fromSource=waitui )** **搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!** 海量技术实践经验,尽在[云加社区](https://cloud.tencent.com/developer?fromSource=waitui)!

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2960 次点击  
加入收藏 微博
上一篇:函数
下一篇:数组与切片
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传