套接字的概念
Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么我们就可以使用文件描述符来引用套接字。
与管道类似,Linux系统将套接字封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。
管道与套接字的区别是:
- 管道主要应用于本地进程间通信
- 套接字多应用于网络进程间数据的传递。
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
套接字通信原理如下图所示:
- 一个文件描述符指向一个套接字
- 一个套接字内部由内核借助两个缓冲区实现,一个写缓冲、读缓冲。
- 数据流动:发送端→
sfd(文件描述符)→cfd(文件描述符)→接收端缓冲区(读缓冲)。并非图上所画直接从发送端到接收端。
在通信过程中, 套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。
TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。
套接字编程简介
网络字节序
- 我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分。
- 网络数据流的顺序:先发出低地址的数据,后发出高地址的数据。
- TCP/IP协议规定,网络数据流应采用大端字节序(低地址高字节)。如果本地主机是小端字节序的,则需要考虑网络字节序和主机字节序的转换问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
|
|
h表示host,n表示network,l表示32位长整数,s表示16位短整数- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
- IP地址是32位,端口号是16位
- 因为IP地址是
string类型,而字节序转换函数接收参数类型是无符号32位长整数,所以在调用字节序转换函数前,还需要调用atoi函数对IP地址进行类型转换。(了解即可,后面有封装的IP 地址转换函数可直接完成IP地址的类型转换和字节序转换)
IP地址转换函数
|
|
- 函数名为
inet_pton,意思是ip to network,将**String类型的IP地址由本地字节序转换无符号32位长整型的网络字节序** af:IP协议类型,- 可选值为
AF_INET、AF_INET6 - 分别代表IPv4和IPv6两种协议。
AF_INET、AF_INET6是宏定义。
- 可选值为
src:传入参数,要转换的String类型的IP地址(点分十进制)dst:传出参数,转换后的网络字节序的IP地址- 函数返回值是
int类型- 转换成功返回
1 - 异常返回
0,说明src指向的不是一个有效的ip地址。 - 失败返回
-1
- 转换成功返回
- 如果函数接口有指针参数,既可以把指针所指向的数据传给函数使用(称为传入参数),也可以由函数填充指针所指的内存空间,传回给调用者使用(称为传出参数)
- 因为
void *可以指向任何类型的数据,所以void *指针一般被称为通用指针或者泛指针,或者叫做万能指针。void指针可以赋值给其他任意类型的指针,其他任意类型的指针也可以赋值给void指针,它们之间赋值不需要强制类型转换。
|
|
- 函数名为
inet_ntop,意思是network to ip,将IP地址由无符号32位长整型的网络字节序转换为String类型的本地字节序 af:AF_INET、AF_INET6src:传入参数,要转换的无符号32位长整型的IP地址的网络字节序dst:传出参数,转换后的String类型的IP地址的本地字节序size:dst的大小,也就是转换后的String类型的IP地址的本地字节序的长度- 返回值:
- 成功:
dst - 失败:
NULL
- 成功:
套接字地址结构
-
因为网络编程函数的诞生早于IPv4协议,那时候使用的是
sockaddr结构体,后来对该结构体改进产生了后面三个结构体类型sockaddr_in中的in代表internetsockaddr_in是IPv4使用的,sockaddr_in6是IPv6使用的sockaddr_un是本地进程间通信套接字(了解即可)
-
由于网络编程函数的传入参数仍是旧的
sockaddr结构体类型,所以在使用网络编程函数时需要使sockaddr_in结构体类型通过强制类型转换为sockaddr结构体类型- c语言中强制类型转换:按照变量的原数据类型从内存取出数据,然后按照另一数据类型的来解释数据
-
sockaddr_in类型的结构体将sockaddr类型的结构体中最后14字节大小的地址数据进行了进一步的细分,将端口号和IP地址分别存在了两个变量中- 16位的地址类型(2字节)
- 16位的端口号(2字节)
- 32位的IP地址(4字节)
- 8字节填充
|
|
|
|
【例】以bind函数为例演示套接字地址结构的使用
|
|
INADDR_ANY:- 是一个宏,自动取系统中有效的任意IP地址,
- 让服务器端计算机上的所有网卡的IP地址都可以作为服务器IP地址,也即监听外部客户端程序发送到服务器端所有网卡的网络请求,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。
- 二进制类型,也就是长整型,所以不需要类型转换。
总结
sockaddr和sockaddr_in二者长度一样,都是16个字节,即占用的内存大小是一致的,前2个字节都表示地址类型,不同的是:- 内部结构
sockaddr用最后的14个字节来表示sa_data,sockaddr_in把最后14个字节拆分成sin_port,sin_addr和sin_zero分别表示端口、IP地址、填充字节sin_zero填充字节使sockaddr_in和sockaddr保持一样大小都是16字节
- 用途
sockaddr是给操作系统使用的,因为早期的系统调用函数指定了参数类型必须是sockaddrsockaddr_in是给程序员使用的,因为sockaddr_in区分了IP地址和端口,使用更方便
- 内部结构
- 程序员把类型、IP地址、端口填充给
sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数
基本TCP套接字编程
网络通信流程图
- 上图表示:借助TCP实现一个CS模型所需要的网络套接字函数
- 一个网络通信的建立需要三个套接字:一对用来通信,一个用来监听
socket()创建一个socketbind()为socket绑定ip+portlisten()设置监听上限,即同时跟服务器建立socket连接的数量accept():阻塞监听客户端连接,创建一个新的socket用来与客户端通信connect():客户端使用现有的socket与服务器建立连接,如果不使用bind绑定客户端地址结构,采用“隐式绑定”,系统自动分配ip+port
TCP协议通讯流程
-
连接的建立:三次握手的过程
- 服务器调用
socket()、bind()、listen()完成初始化后,调用**accept()阻塞等待**,处于监听端口的状态 - 客户端调用
socket()初始化后,调用**connect()发出SYN段(SYN状态标志位有效)并阻塞等待**服务器应答 - 服务器应答一个
SYN-ACK段 - 客户端收到后从**
connect()返回**,同时应答一个ACK段,服务器收到ACK段后从**accept()返回**
- 服务器调用
-
数据传输的过程
- 建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。
- 服务器从
accept()返回后立刻调用read(),读socket的缓冲区就像读管道一样,如果没有数据到达**read()就阻塞等待** - 客户端调用
write()发送请求给服务器,然后,调用**read()阻塞**等待服务器的应答, - 服务器收到客户端的请求进行读取后**
read()返回**,处理请求后,调用write()将处理结果发回给客户端,再次调用**read()阻塞**等待下一条请求 - 客户端收到后从**
read()返回**,发送下一条请求,如此循环下去
-
连接的释放:
-
如果客户端没有更多的请求了,就调用
close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。 -
注意:
-
半关闭状态:通信双方中,只有一端关闭通信
-
调用
close()后,调用方关闭通信,调用方的读和写缓冲区都关闭 -
调用
shutdown()后,调用方关闭通信,但可指定函数参数来只关闭写缓冲或读缓冲
-
-
TCP状态转换
- 实线表示主动发起连接一端的状态
- 虚线表示被动接收连接一端的状态
主动发起连接请求端
CLOSED:表示初始状态SYN_SENT:主动打开状态,表示客户端已发送SYN报文。这个状态与SYN_RCVD遥相呼应,当客户端发送SYN报文(第一次握手)后,随即进入到SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。ESTABLISHED:数据传输状态,表示连接已经建立。当客户端收到服务器应答的SYN-ACK报文(第二次握手),并回复ACK报文(第三次握手)后,随即进入到ESTABLISHED状态
主动关闭连接请求端
FIN_WAIT_1:主动关闭连接的一方,发出FIN报文后进入该状态FIN_WAIT_2:半关闭状态,该状态下的socket只能接收数据,不能发送数据。当主动关闭连接的一方,发出FIN收到ACK以后进入该状态TIME_WAIT: 当收到了对方的FIN报文,并发送出了ACK报文后,进入到TIME_WAIT状态,CLOSE:TIME_WAIT状态等2MSL后即可回到CLOSED初始状态- 如果
FIN_WAIT_1状态下,收到对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态 - 只有主动关闭连接方会经历该状态
- 如果
2MSL时长:一定出现在【主动关闭连接请求端】。对应TIME_WAIT状态,保证最后一个ACK能成功被对端接收。(等待2MSL时长期间,对端没收到我发的ACK,对端会再次发送FIN请求)
被动接收连接
CLOSED:表示初始状态LISTEN:监听状态,表示服务器端可以接受连接请求SYN_RCVD: 当接收到SYN报文(第一次握手),并回复SYN-ACK报文(第二次握手)后,进入SYN_RCVD状态ESTABLISHED:数据传输状态,表示连接已经建立。当服务器接收到客户端应答的ACK报文(第三次握手)后,随即进入到ESTABLISHED状态
被动关闭连接
CLOSE_WAIT:当接收到主动关闭方发送的FIN并应答一个ACK报文后,进入到CLOSE_WAIT状态。接下来查看是否还有数据发送给对方,如果没有,则发送FIN报文给对方- 说明对端(主动关闭连接端)处于半关闭状态
LAST_ACK: 当被动关闭一方在发送FIN报文后,进入LAST_ACK状态CLOSE:当被动关闭一方在LAST_ACK状态接收到对方发送的ACK后进入CLOSE初始状态,即关闭连接
socket函数
|
|
domain:IP协议,AF_INET、AF_INET6、AF_UNIX/AF_LOCALtype:数据传输协议,流式协议SOCK_STREAM,报式协议SOCK_DGRAMprotocol:数据传输协议中的代表协议。- 默认
0,根据type选择指定协议。 - 流式协议
SOCK_STREAM代表协议TCP - 报式协议
SOCK_DGRAM代表协议UDP
- 默认
- 返回值
- 成功:新套接字对应的文件描述符
- 失败:
-1 errno,
【例】用socket()函数创建一个套接字,使用IPv4协议、流式协议,返回该套接字的文件描述符
|
|
bind函数
|
|
sockfd:套接字对应的文件描述符addr:(struct sockaddr)&addraddr要先初始化
addrlen:sizeof(addr),地址结构的大小- 返回值
- 成功:
0 - 失败:
-1 errno
- 成功:
【例】用bind函数绑定socket地址结构
|
|
listen函数
|
|
sockfd:套接字对应的文件描述符backlog:上限数值,最大值128- 返回值
- 成功:
0 - 失败:
-1 errno
- 成功:
accept函数
|
|
-
sockfd:套接字对应的文件描述符 -
addr:传出参数。成功与服务器建立连接的客户端的地址结构(ip+port) -
addrlen:传入传出参数。&client_addr_len- 传入的是调用者提供的
addr缓冲区的长度以避免缓冲区溢出问题- 使用前需要先定义
addr缓冲区的长度 - 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给
addr参数传NULL,表示不关心客户端的地址
- 使用前需要先定义
- 传入的是调用者提供的
-
返回值
- 成功:能与服务器进行数据通信的客户端
socket对应的文件描述符 - 失败:
-1 errno
- 成功:能与服务器进行数据通信的客户端
connect函数
|
|
sockfd:客户端套接字对应的文件描述符addr:传入参数。服务器的地址结构addrlen:服务器的地址结构的大小- 返回值
0:成功-1 errno:失败
- 如果不使用
bind绑定客户端地址结构,采用“隐式绑定”,系统自动分配ip+port
【例】用connect函数请求连接服务器
|
|
setsockopt函数
|
|
- 函数功能:用来设置参数
sockfd所指定的socket的状态 level:代表欲设置的网络层,SOL_SOCKET:socket层
optname:代表欲设置的选项参数SO_REUSEADDR:地址复用
optval:代表欲设置的值1:启用0:不启用
optlen:为optval的长度
多进程并发服务器
使用多进程并发服务器时要注意:
- 防止僵尸进程
- 捕捉SIGCHLD信号
|
|
-
Socket():创建监听套接字listenfdlistenfd是用来建立连接的套接字- 当客户端有连接请求时,服务器会借助
listenfd创建一个connectfd用来与客户端进行通信数据传输。listenfd被解放出来,用于监听其他客户端的请求 - 在多进程并发服务器中,父进程用于监听客户端请求,子进程用于与客户端通信
-
Bind():绑定地址结构struct socketaddr_in servaddr; -
Listen():设置同时与服务器建立连接的上限数(同时进行三次握手的客户端) -
Accept():阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的connectfd- 在多进程并发服务器中,每个
connectfd都有一个子进程与之对应; - 在多进程并发服务器中,由于支持多个客户端与服务器建立连接,所以
Accept应该循环执行,一直监听客户端的连接请求
- 在多进程并发服务器中,每个
-
fork():产生子进程 -
Close():fork产生子进程后,子进程和父进程一样,都有listenfd和connectfd两个套接字。子进程不需要监听套接字,只需要与客户端通信的套接字,所以关闭listenfd;父进程不需要与客户端通信的套接字,只需要监听套接字用来监听客户端请求,所以关闭connectfd -
read():读缓冲 -
将读到的字节进行操作
-
write():写缓冲 -
signal():信号捕捉,如果捕捉到SIGCHLD信号,说明子进程结束,就调用处理函数,完成子进程回收 -
waitpid:因为waitpid()可以设置不阻塞,所以使用waitpid()。- 由于设置
waitpid()为非阻塞,所以当子进程没结束时,返回0,需要再次执行去回收,防止所有子进程都变为僵尸进程,所以需要设置为循环执行waitpid(),子进程一旦结束,一定要立刻回收。但循环执行的话,父进程可能会阻塞于waitpid(),此时如果有新的客户端连接请求,父进程无法响应。 - 结局办法:将上述操作放入信号处理函数中
- 由于设置
-
continue:结束本次循环,父进程监听下一个客户端的连接请求
多路I/O转接服务器
原理:借助内核,select或poll来监听客户端连接、数据通信事件

select函数
|
|
-
函数功能:用于监视文件描述符的变化情况——读/写/异常
-
nfds:监听的所有文件描述符中最大的一个+1,此参数会告诉内核监听多少个文件描述符 -
readset:传入传出参数,读文件描述符的监听集合,只监听该集合中文件描述符的读事件- 传入传出参数:传入的是想要监听的文件描述符,函数执行完,传出的是满足对应事件的文件描述符
-
writeset:传入传出参数,写文件描述符的监听集合,只监听该集合中文件描述符的写事件NULL:不监听文件描述符的写事件
-
exceptset:传入传出参数,异常文件描述符的监听集合,只监听该集合中文件描述符的异常事件NULL:不监听文件描述符的异常事件
-
timeout:阻塞监听时间NULL:阻塞,一直等timeval:timeval是一个结构体,有两个成员变量:tv_sec和tv_usec,分别是秒和微秒>0:设置成员变量为固定时间0:不阻塞,检查描述字后立即返回,轮询
-
返回值:
>0:所有的文件描述符监听集合中满足对应事件的总个数0:没有满足对应事件的文件描述符-1:errno
-
优点
- 跨平台:
win、linux、macOS、Unix、类Unix、mips
- 跨平台:
-
缺点
- 监听上限受文件描述符限制,最大 1024
- 无法直接定位满足监听事件的文件描述符,只能用循环轮询或自己构建数组保存各个
connfd
|
|
思路分析:
|
|
poll函数
|
|
- 函数功能:用于监视文件描述符的变化情况——读/写/异常
fds:监听的文件描述符的结构体【数组】,此处的*就是首地址fd:监听的文件描述符events:监听事件,用宏定义POLLIN:有数据可读POLLOUT:写事件POLLERR:异常事件POLLRDNORM:有普通数据可读
revents:初始化为0,如果满足对应事件则返回非0,即POLLIN、POLLOUT、POLLERR
nfds:监听数组中的实际监听个数timeout:阻塞监听时间,单位:毫秒-1:阻塞,一直等0:不阻塞,检查描述字后立即返回,>0:等待指定毫秒数
- 返回值:
>0:所有的文件描述符监听集合中满足对应事件的总个数0:没有满足对应事件的文件描述符-1:errno
- 优点
- 自带数组结构,分离了传入传出参数
- 可以拓展监听上限,超出 1024限制
- 缺点
- 不能跨平台,只能UNIX/LINUX,不能WINDOWS
- 无法直接定位满足监听事件的文件描述符,只能用循环轮询或自己构建数组保存各个
connfd
基本UDP套接字编程
TCP和UDP对比
-
TCP:传输控制协议
- 面向连接的,可靠数据包传输。
- 对于不稳定的网络层,采取完全弥补的通信方式——丢包重传
- 优点:
- 数据流量稳定:在建立TCP连接后,数据传输流量是稳定的
- 传输速度稳定:在建立TCP连接后,数据传输速度是稳定的
- 传输顺序一致:在网络环境不发生变化的情况下,各个数据包经过的路由节点一致,数据报到达顺序与发送时一致
- 缺点:
- 传输速度慢
- 传输效率低
- 资源开销大
- 使用场景
- 数据的完整型要求较高,不追求效率
- 大数据传输、文件传输。
- 数据的完整型要求较高,不追求效率
-
UDP:用户数据报协议
- 无连接的,不可靠的数据报传递
- 对于不稳定的网络层,采取完全不弥补的通信方式——默认还原网络状况
- 优点:
- 传输速度快
- 传输速率高
- 资源开销小
- 缺点:
- 数据流量不稳定
- 传输速度不稳定
- 传输顺序不一致:各个数据报经过的路由节点可能不一致,数据报到达顺序可能与发送时不一致
- 使用场景
- 对时效性要求较高场合,稳定性其次
- 游戏、视频会议、视频电话
- 对时效性要求较高场合,稳定性其次
- 可以通过采用应用层数据校验协议,弥补udp的不足,保证数据包有效传递
UDP通信流程
|
|
- 因为不用三次握手,所以服务器不需要
accept(),客户端不需要connect() - 因为
listen()是用来设置服务器同时与客户端进行三次握手的上限,所以listen()可有可无 - 由于
listenfd不负责通信,所以read()被替换为recvfrom(),write()被替换为sendto() - 客户端直接使用
sendto()向服务器发起连接
recvfrom函数
|
|
- 函数功能:接收
socket传来的数据, 并把数据存到buf指向的内存空间 sockfd:对服务器来说就是listenfdbuf:接收数据的缓冲区len:缓冲区大小flags:调用操作方式0:常规操作,与read()相同
src_addr:传出参数,传出对端地址结构addrlen:传入传出参数,地址结构长度- 返回值:
>0:成功接收到的字节数0:对端关闭连接-1:errno
sendto函数
|
|
- 函数功能:将数据由
socket传给对方主机 sockfd:套接字buf:存储数据的缓冲区len:数据长度flags:调用操作方式0:常规操作,与write()相同
src_addr:传入参数,传入目标地址结构addrlen:传入传出参数,地址结构长度- 返回值:
>0:成功写出的字节数0:对端关闭连接-1:errno
Linux和C语言知识
包裹函数
- 包裹函数用于错误处理
- 任何现实世界的程序都必须检查每个函数调用是否返回错误
- 每个包裹函数完成实际的函数调用,检查返回值,并在发生错误时终止进程
- 约定包裹函数名是实际函数名的首字母大写形式
Unix errno值
- 只要一个Unix函数中有错误发生,全局变量
errno就被置为一个指明该错误类型的正值,函数本身则通常返回-1 err_sys查看errno变量的值并输出相应的出错消息后退出进程- 例如:当
error的值等于ETIMEDOUT时,将输出Connection timed out
- 例如:当
error的值只在函数发生错误时设置,如果函数不返回错误,error的值就没有定义error的所有正数错误值都是常值,具有以E开头的全大写字母名字,值0不表示任何错误
sizeof操作符
sizeof是C语言的一种单目操作符,如C语言的其他操作符++、--等sizeof操作符以字节形式给出了其操作数的存储大小- 操作数可以是一个表达式或括在括号内的类型名。操作数的存储大小由操作数的类型决定
文件描述符
文件描述符是Linux编程中的一个术语。当一个文件打开后,系统会分配一部分资源来保存该文件的信息,以后对文件的操作就可以直接引用该部分资源了。文件描述符可以认为是该部分资源的一个索引,在打开文件时返回。
当程序执行时,就已经有三个文件流打开了,它们分别是标准输入stdin,标准输出stdout和标准错误输出stderr。和流式文件相对应的是,也有三个文件描述符被预先打开,它们分别是STDIN_FILENO:0,STDOUT_FILENO:1,STDERR_ FILENO:2,代表标准输入、标准输出和标准错误输出。
需要指出的是,上面的流式文件输入、输出和文件描述符的输入输出方式不能混用,否则会造成混乱
fileno函数
|
|
- 函数功能:把文件流指针转换成文件描述符
- 返回值:
- 指定文件流的文件描述符:成功
-1:失败
fputs函数
|
|
- 函数功能:把字符串写入到指定的流
stream中,但不包括空字符 str:要写入的字符数组,以空字符终止stream:要被写入字符串的流,是指向FILE对象的指针- 返回值:
- 非负值:成功
EOF:发生错误
fgets函数
|
|
- 函数功能:从指定的流
stream读取一行,并把它存储在str所指向的字符串内。当读取(n-1)个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定 str:存储读取的字符的字符数组。n:要读取的最大字符数(包括最后的空字符)stream:指向FILE对象的指针,标识了要从中读取字符的流- 返回值:
str参数:成功NULL:- 到达文件末尾或者没有读取到任何字符,
str的内容保持不变,并返回一个NULL - 发生错误
- 到达文件末尾或者没有读取到任何字符,
bzero函数
|
|
- 函数功能:将内存块或字符串的前
n个字节清零 s:内存或字符串指针n:需要清零的字节数
头文件
#include <>格式:引用标准库头文件,编译器从标准库目录开始搜索#include""格式:引用非标准库的头文件,编译器从用户的工作目录开始搜索
命令行参数
|
|
argc:int类型,表示命令行参数的个数- 不允许用户传递,自动根据用户从命令行输入的参数个数确定
argv:char**类型,用于存储用户从命令行传递进来的参数- 它的第一个成员是用户运行的程序名字
read和write函数
read函数
|
|
- 作用:从文件描述符相关联的文件里读入
count个字节的数据,并把它们放到数据区buf中。 - 返回值:实际读入的字节数,这可能会小于请求的字节数,
0,已经读到套接字末尾(对端已经关闭)-1,表示read()调用出现了错误,错误代码保存在全局变量errno中EINTR:被异常中断,需要重启EAGIN/EWOULDBLOCK:以非阻塞方式读数据,但没有数据,需要再次读ECONNRESET:连接被重置,需要close(),移出监听队列
>0,表示读取的字节数
write函数
|
|
- 作用:把缓冲区的前
count个字节写入与文件描述符关联的文件。 - 返回值:
write()返回实际写入的字节数- 返回
0,表示未写入任何数据 - 返回
-1,表示write()调用中出现了错误,错误代码保存在全局变量errno中。 - 返回
>0,表示写入的字节数
- 返回
write()可能会报告写入的字节比要求的少,这不一定是错误,在程序中,需要检查errno以发现错误,然后再次调用write()写入剩余数据
readn,writen和readline函数
read()和write()函数在字节流套接字上表现的行为和通常的文件I/O不同,字节流套接字上调用read()和write()输入或输出的字节数可能比请求的数量少,因为内核中用于套接字的缓冲区是有限制的,需要调用者多次调用read()或write()函数。
提示:
readn()、writen()和readline()是对read()和wirte()的封装
|
|
-
readn()函数功能:在网络编程的读取数据中,通常会需要用到一个读指定字节才返回的函数,linux系统调用中没有给出,需要自己封装。- 当剩余长度大于0的时候就一直读啊读
- 当read的返回值小于0的时候,做异常检测
- 当read的返回值等于0的时候,退出循环
- 当read的返回值大于0的时候,拿剩余长度减read的返回值,拿到新的剩余长度,读的入口指针加上read的返回值,进入步骤1
- 返回参数n减去剩余长度,即实际读取的总长度
-
writen()函数功能:写满n个字节才返回- 当要写入的剩余长度大于0的时候就一直写啊写
- 当write的返回值小于0的时候,做异常检测
- 当write的返回值等于0的时候,出错退出程序
- 当write的返回值大于0的时候,拿剩余长度减去write的返回值,拿到新的剩余长度,写的入口指针加上write的返回值,进入步骤1
- 返回参数n的值,即期望写入的总长度
-
readline()函数功能:读到'\n'或者读满缓冲区才返回- 当读取的次数小于maxlen的时候就一直读啊读
- 当read的返回值等于1的时候,将读到的值赋值给
ptr++,判断是不是\n - 当read的返回值等于0的时候,读完了
- 当read的返回值小于0的时候,做异常检测
- 进入步骤1
- 返回实际读取的长度
close和shutdown函数
close函数
|
|
close一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,也就是说它不能再作为read()或write()的第一个参数- 然而,TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。
- 在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程
close掉套接字时,描述符引用计数会相应的减一,当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断连过程
shutdown函数
|
|
该函数的行为依赖于howto的值
SHUT_RD:值为0,关闭连接的读这一半。SHUT_WR:值为1,关闭连接的写这一半。SHUT_RDWR:值为2,连接的读和写都关闭。
终止网络连接的通用方法是调用close()函数。但使用shutdown()能更好的控制断连过程(使用第二个参数)
close和shutdown区别
-
使用
close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。 -
shutdown不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。 注意:-
如果有多个进程共享一个套接字,
close每被调用一次,计数减1,直到计数为0时,也就是所有进程都调用了close,套接字将被释放。 -
在多进程中如果一个进程调用了
shutdown(sockfd, SHUT_RDWR)后,其它的进程将无法进行通信。但,如果一个进程close(sockfd)将不会影响到其它进程。
-
-
shutdown在关闭多个文件描述符引用的文件时,采用全关闭方法;close只关闭一个
sleep函数
sleep(time)函数以秒为单位,将进程由运行态转入阻塞态,使进程睡眠;当睡眠时间结束,进程由阻塞态转为就绪态,等待CPU空闲后进入运行态继续执行剩下的指令- 这个函数是可以被中断的
- 当进程在睡眠的过程中,如果被中断,那么当中断结束,回来再执行该进程的时候,该进程会从
sleep()函数的下一条语句执行;这样的话就不会睡眠time秒了
- 当进程在睡眠的过程中,如果被中断,那么当中断结束,回来再执行该进程的时候,该进程会从
进程控制
fork函数
|
|
-
函数功能:创建子进程。
-
原理分析:执行
fork()后,由父进程复制产生子进程,两个进程都由fork()之后的代码继续向下执行,父进程返回子进程的pid,子进程返回0- 当返回值是
0,说明这是子进程;当返回值>0,也就是子进程的pid,说明这是父进程; - 父子进程相同:
data段、code段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式 - 父子进程不同:
pid、fork返回值、各自的父进程、进程创建时间、闹钟、未决信号集 - 父子进程共享: 读时共享、写时复制。读的时候不复制,父子进程用同一份数据;写的时候需要一份复制后写,写后不影响另一进程
- 当返回值是
-
返回值:
- 成功:父子进程各自返回,父进程返回子进程的
pid,子进程返回0 - 失败:
-1errno
- 成功:父子进程各自返回,父进程返回子进程的
exec函数族
exec函数族中的函数可以使fork()出的子进程不执行复制来的父进程的代码,转而执行另一个程序,子进程的代码和数据完全被新程序替换
回收子进程
孤儿进程
父进程先于子进程终止,子进程成为孤儿进程,会被 init 进程领养
僵尸进程
进程终止,父进程存在但尚未回收该子进程,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程
僵尸进程无法使用kill命令清除,因为kill命令只是用来终止进程的,而僵尸进程已经终止
wait函数
一个进程终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着。内核在PCB保存了一些信息:
- 如果是正常终止则保存着退出状态,
- 如果是异常终止则保存着导致该进程终止的信号
这个进程的父进程可以调用
wait()或者waitpid()获取PCB中保存的信息,然后彻底清除掉这个进程
|
|
- 函数功能:父进程调用
wait()函数可以回收子进程,该函数具体有以下两个功能- 阻塞等待子进程退出:
wait()函数一直阻塞等待子进程的结束;子进程结束后,回收该子进程,wait()函数返回 - 获取子进程残留资源:回收PCB中记录的子进程的退出状态和子进程结束的原因
- 阻塞等待子进程退出:
status:传出参数,保存子进程残留资源NULL:不保存子进程残留资源
- 返回值:
- 成功:子进程ID
- 失败:
-1
waitpid函数
|
|
-
函数功能:
- 作用同
wait():用来回收子进程; - 但可以指定
pid回收, - 且可以通过设置
options,使子进程即使没结束,也可以直接回收,使waitpid()函数不阻塞
- 作用同
-
pid:>0:回收指定ID的子进程0:回收同一进程组的任意子进程-1:回收任意子进程
-
statusNULL:不关心子进程的结束状态
-
options:WNOHANG:不阻塞模式- 如果该子进程没有结束,则不回收子进程,返回
0; - 如果该子进程已经结束,则回收该子进程,返回
pid;
- 如果该子进程没有结束,则不回收子进程,返回
-
返回值:
>0:成功,子进程ID-1:失败0:options指定为WNOHANG,且子进程未结束
进程通信
进程间通信的常用方式:
- 管道:使用最简单
- 信号:占用CPU和内存的开销最小,携带数据量有限,表示的数据单一,应用在对速度要求高的进程之间
- 共享映射区:应用在无血缘关系的进程之间
- 本地套接字:最稳定
信号的概念
- 信号是软件层面上的“中断”。
- 一旦信号产生,无论程序执行到什么位置,必须立即停止运行转而处理信号,待信号处理结束再继续执行后续指令
- 所有信号的产生及处理全部都是由内核完成的
- 每个进程收到的所有信号,都是由内核负责发送的,内核处理
信号的产生
产生信号的方式:
- 按键产生
- 系统调用产生
- 软件条件产生
- 硬件异常产生
- 命令产生
信号集的概念
- 递达:产生并且送达到进程。直接被内核处理掉。
- 未决:产生与递达之间的状态。
- 信号处理方式:
- 默认处理动作
- 忽略
- 捕捉(自定义处理动作)
- 阻塞信号集(信号屏蔽字): 本质是位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决状态
- 未决信号集:本质是位图。用来记录信号的处理状态。该信号集中的信号,
1表示已经产生,但尚未被处理现处于未决状态;0表示处理完成 - 信号集存储在PCB中
信号的四要素
-
使用
kill -l命令可以查看当前系统可使用的信号- 不存在编号为0的信号
- 1-31号信号是常规信号(普通信号/标准信号)
- 34-64号信号是实时信号,与驱动编程和硬件相关
-
信号的四要素
- 信号编号
- 信号名称
- 信号对应事件
- 信号默认处理动作
- Term:终止进程
- Ign:忽略信号
- Core:终止进程,生成Core文件
- Stop:停止(暂停)进程
- Cont:继续运行进程
-
只有信号对应的事件发生了,该信号才会被递送(但不一定递达)
信号捕捉
信号捕捉:捕捉到信号后,自定义处理动作
signal函数
注册一个信号捕捉函数
|
|
- 该函数由
ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为 - 第一句:定义了一个指针变量
sighandler_t,该指针变量可以指向返回值类型为空,且有一个整型参数的函数sighandler_t的类型为void(*)(int) signum:注册捕捉哪个信号handler:捕捉到信号后的行为
SIGCHLD信号
- 子进程状态发生变化时,父进程会收到
SIGCHLD信号- 子进程终止时
- 子进程接收到
SIGSTOP信号停止时 - 子进程处在停止态,接收到
SIGCONT唤醒时
- 默认动作为:忽略这个信号
- 可以通过信号捕捉,自定义处理动作
守护进程
- 守护进程(Daemon)是后台进程,没有控制终端,周期性(循环)地执行某种任务或等待处理某些发生的事件,命名一般以
d结尾 - 创建守护进程,最关键的一步是使用
setid函数创建一个新的Session,并成为Session Leader
会话是多个进程组的集合,进程组是多个进程的集合