值得收藏的TCP套接口编程文章
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~
TCP客户端-服务器典型事件
下图是TCP客户端与服务器之间交互的一系列典型事件时间表:
- 首先启动服务器,等待客户端连接
- 启动客户端,连接到服务器
- 客户端发送一个请求给服务器,服务器处理请求,响应客户端
- 循环步骤3
- 客户端给服务器发一个文件结束符,关闭客户端连接
- 服务器也关闭连接
基本TCP客户-服务器程序的套接口函数
套接口编程基本函数
socket 函数
为了执行网络I/O,一个进程(无论是服务端还是客户端)必须做的第一件事情就是调用socket函数。
#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服务器的连接。
#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函数
函数bind为套接口分配一个本地协议地址,包括IP地址和端口号。
#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服务器调用。
#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表示内核为此套接口排队的最大连接数。对于给定的监听套接口,内核会维护两个队列:
未完成连接队列(incomplete connection queue) SYN分节已由客户发出,到达服务器,正在进行TCP的三路握手。此时这些套接口处于
SYN_RCVD状态。已完成连接队列(completed connection queue) SYN分节已由客户发出,到达服务器,并且已完成三路握手。此时这些套接口处于
ESTABLISHED状态。当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新条目,直到三路握手中,第三个分节(客户对服务SYN的ACK)到达,这个条目移到已完成连接队列的队尾。
当进程调用
accept函数时,已完成连接队列的头部条目返回给进程。两个队列之和不能超过
backlog当一个客户SYN到达时,若这两个队列都是满的,TCP就忽略此分节,且不发送RST。客户TCP将重发SYN,期望不久就能在队列中找到空闲位置。
TCP为监听套接口维护的两个队列
accept函数
函数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地址,临时端口号也发生了变化。
fork和exec函数
#include <unistd.h>
pid_t fork(void);/* 返回:在子进程中为0,在父进程中为子进程ID,-1——出错 */
fork函数调用一次,却返回两次。
- 在调用它的进程(即父进程),它返回一次,返回值是派生出来的子进程的进程ID。 父进程可能有很多子进程,必须通过返回值跟踪记录子进程ID。
- 在子进程,它还返回一次,返回值为0。 子进程只有一个父进程,总可以通过
getppid来得到父进程的ID
通过返回值可以判断当前进程是子进程还是父进程。
父进程在调用fork之前打开的所有描述字在函数fork返回后都是共享的。网络服务器会利用这一特性:
- 父进程调用
accept。 - 父进程调用
fork,已连接套接口就在父进程与子进程间共享。(一般来说就是子进程读、写已连接套接口,而父进程关闭已连接套接口)。
fork有两个典型应用:
- 一个进程为自己派生一个拷贝,并发执行任务,这也是典型的并发网络服务器模型。
- 一个进程想执行其他的程序,于是调用
fork生成一个拷贝,利用子进程调用exec来执行新的程序。典型应用是shell。
以文件形式存储在硬盘上的可执行程序若要被执行,需要由一个现有进程调用exec函数。我们将调用exec的进程称为调用进程,新程序的进程ID并不改变,仍处于当前进程。
小结
客户和服务器,从调用socket开始,返回一个套接口描述字。客户调用connect,服务器调用bind、listen、accept。最后套接口由close关闭。
多数TCP服务器是调用fork来实现并发处理多客户请求的。多数UDP服务器则是迭代的。
相关阅读
系统重启后nginx reload不生效原因分析
SRS开源直播服务 - StateThreads微线程框架学习
高性能网络编程3----TCP消息的接收
【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识
此文已由作者授权腾讯云+社区发布,更多原文请点击
搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!
海量技术实践经验,尽在云加社区!
值得收藏的TCP套接口编程文章的更多相关文章
- UNIX网络编程读书笔记:基本TCP套接口编程
编写一个完整的TCP客户和服务器程序所需要的基本套接口函数: 1.socket函数(客户端.服务器端都必须调用) 参数family指明协议族(family),该参数也往往被称为协议域(domain). ...
- Linux C 网络编程——3. TCP套接口编程
1. 基本流程 2. socket() int socket(int domain, int type, int protocol); socket()打开一个网络通讯端口,如果成功的话,就像open ...
- 【转】Linux C 网络编程——TCP套接口编程
地址:http://blog.csdn.net/matrix_laboratory/article/details/13669211 2. socket() <span style=" ...
- UNIX网络编程读书笔记:基本SCTP套接口编程
概述 SCTP是一个较新的传输协议,于2000年在IETF得到标准化(TCP是在1981年标准化的).它最初是为满足不断增长的IP电话市场设计的:具体地说,就是穿越因特网传输电话信令. SCTP是一个 ...
- UNIX网络编程读书笔记:基本UDP套接口编程
概述 使用UDP编写的一些流行的应用程序有:DNS(域名系统).NFS(网络文件系统)和SNMP(简单网络管理协议). 如下图所示,给出了典型的UDP客户/服务器程序的函数调用: 客户不与服务器建立连 ...
- UDP套接口编程
常用的UDP实现的程序:DNS域名系统,NFS网络文件系统,SNMP简单网络管理协议 ssize_t recvfrom(int sockfd,void *buff,size_t nbytes,int ...
- UNIX网络编程读书笔记:套接口选项
概述 有很多方法来获取和设置影响套接口的选项: getsockopt和setsockopt函数 fcntl函数 ioctl函数 getsockopt和setsockopt函数 这两个函数仅用于套接口. ...
- UNIX网络编程读书笔记:端口号、套接口对和套接口
端口号 端口号(port number):16位整数,用来区分不同的进程. 服务器使用的端口号:TCP和UDP定义了一组众所周知的端口(well-known port),用于标识众所周知的服务. 客户 ...
- 基于socket的TCP和UDP编程
一.概述 TCP(传输控制协议)和UDP(用户数据报协议是网络体系结构TCP/IP模型中传输层一层中的两个不同的通信协议. TCP:传输控制协议,一种面向连接的协议,给用户进程提供可靠的全双工的字节流 ...
随机推荐
- jquery添加select option两种代码思路比较
功能需求:在客户选择了check_in_date和check_out_date之后,将在check_in_date至check_out_date的promotions中自动添加符合条件的promoti ...
- PO Release Final Closed 灾难恢复
今天不小心 Final Closed了一条Po Release,只能通过后台更新数据恢复了. 更新后可接收可匹配,但不保证更新数据有遗漏,慎用. 更新前备份各表数据 UPDATE PO_LINE_LO ...
- C# 委托和接口
能用委托解决的事情,接口也都可以解决.如下所示: public static void Main() { , , , }; Util.TransformAll(values, new Squarer( ...
- .Net Core 跨平台应用使用串口、串口通信 ,可能出现的问题、更简洁的实现方法
前些天在学习在 .NET Core下,跨平台使用串口通讯,有一篇文章说到在Linux/物联网下,实现通讯. 主要问题出现在以下两个类库 SerialPortStream flyfire.CustomS ...
- 第五章 JVM垃圾收集器(1)
说明:垃圾回收算法是理论,垃圾收集器是回收算法的实现,关于回收算法,见<第四章 JVM垃圾回收算法> 1.七种垃圾收集器 Serial(串行GC)-- 复制 ParNew(并行GC)-- ...
- 基于duilib的虚拟列表实现
本文由作者邹启文授权网易云社区发布. 在邮箱大师选择duilib作为UI开发库后,我们面临这样一个问题.随着时间的积累,用户数据会越来越多,如何保证我们的软件在展示这些数据时依然保持非常好的体验? 原 ...
- 理解DDoS防护本质:基于资源较量和规则过滤的智能化系统
本文由 网易云发布. 随着互联网生态逐渐形成,DDoS防护已经成为互联网企业的刚需要求,网易云安全(易盾)工程师根据DDoS的方方面面,全面总结DDoS的攻防对抗. 1.什么是DDoS DDoS全称 ...
- koa和egg项目webpack热更新实现
背景 在用Node.js+Webpack构建的方式进行开发时, 我们希望能实现修改代码能实时刷新页面UI的效果. 这个特性webpack本身是支持的, 而且基于koa也有现成的koa-webpack- ...
- C++解析头文件-Qt自动生成信号定义
目录 一.概述 二.实现思路 三.代码讲解 1.类图 2.QtCppDescription 3.测试 四.源代码 一.概述 上一篇文章C++解析头文件-Qt自动生成信号声明我们主要讲解了怎么去解析C+ ...
- OI动态规划&&优化 简单学习笔记
持续更新!! DP的难点主要分为两类,一类以状态设计为难点,一类以转移的优化为难点. DP的类型 序列DP [例题]BZOJ2298 problem a 数位DP 常用来统计或者查找一个区间满足条件的 ...