第二部分:基本套接字编程

IPv4网际套接字地址结构:
struct in_addr{
in_addr_t s_addr;
};

struct sockaddr_in{
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
}

32位的IPv4地址存在两种不同的访问方法,比如,如果serv定义为某个网际套接字地址结构,那么serv.sin_addr将按in_addr结构引用其中的32位地址;如果serv.sin_addr.in_addr_t将引用一个32位无符号整型的地址。

通用套接字地址结构
struct sock_addr{
uint8_t sa_len;
sa_family_t sin_family;
char sa_data[14];
}

IPv6 网际套接字结构
struct in6_addr{
uint8_t s6_addr[16];
};

struct asockaddr_in6{
uint8_t sin6_len;
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};

新的通用套接字地址结构
struct sockaddr_storage{
uint8_t ss_len;
sa_family_t sin_family;
};

网际协议使用大端字节序传送这些多字节整数

主机字节序与网络字节序之间互相转换
uint16_t htons(uint16_t host16bitvalue);
uint32_t htons(uint32_t host32bitvalue);

uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohs(uint32_t net32bitvalue);
其中n代表network,h代表host,s代表short,l代表long

常用函数:
void bzero(void dest, size_t nbytes);
void bcopy(const void
src, void dest, size_t nbytes);
int bcomp(const void
ptr1, const void* ptr2, size_t nbytes);

void* memset(void dest, int c, size_t len);
void
memcpy(const void* src, void dest, size_t nbytes);
int memcmp(const void
ptr1, const void* ptr2, size_t nbytes);

int inet_aton(const char* strptr, struct in_addr* addrptr);
in_addr_t* inet_addr(const char* strptr);
char* inet_ntoa(struct in_addr inaddr);

套接字
int socket(int family, int type, int protocol);
 协议域 套接字类型 0
一般情况:int socket(AF_INET, SOCK_STREAM, 0);
返回-1表示失败,成功返回sockfd;

int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);
成功返回0,出错返回-1
(1)若TCP客户没有收到SYN的响应,会返回ETIMEDOUT;
(2)若TCP客户收到的响应为RST,表示服务器的指定端口上没有进程正在运行并与之连接;产生这种响应的情况如下:(1)目的地为某端口的SYN到达,但该端口上没有在监听的服务器;(2)TCP想取消一个已有的连接;(3)TCP接收到一个根本不存在的TCP分节。
(3)若客户发出的SYN在中间的某个路由器上面接收到”destination unreachable ”,ICMP错误。

bind函数把一个本地协议地址赋予一个套接字。
int bind(int sockfd, const struct sockaddr* myaddr, socklen_t len);
成功返回0,失败返回-1

TCP服务器而言,进程可以将IP地址捆绑到它的套接字上面,与TCP客户连接的时候只能连接IP地址为绑定的IP地址的端口。
连接套接字时,内核需要通过外出网络接口选择源IP地址;如果TCP服务器没有IP地址捆绑到套接字上,则通过TCP客户发送过来的SYN目的IP地址作为源IP地址。

listen函数
int listen(int sockfd, int backlog);
第二个参数为相应的套接字排队的最大的连接个数
内核为任何一个监听套接字维护两个队列 : (1)未完成连接队列:已由服务器到达客户,但是在等待三路握手的过程;(2)已完成连接队列:已完成三路握手的客户。两队列之和不能超过backlog的数目。
模糊因子:把它乘以1.5得到未连接的处理队列的最大长度;相当于backlog设置为5,但是允许最多8个客户端连接。backlog不能设置为0
RTT:未完成连接队列中的任何一项在这个客户端中留存的时间
一个SYN到达时,若未完成连接队列是满的,TCP会忽略该分节。

accept函数
int accept(int sockfd, const struct sock_addr* cliaddr, int len);
成功返回非负描述符,失败返回-1.

fork函数
pid_t fork(void);
在子进程中为0,在父进程中为子进程ID,若出错则为-1
子进程可以通过getppid获取父进程的进程ID,而父进程需要记录fork的返回值来获取进程ID。
fork函数的典型用法:
1.一个进程创建自身的副本,这样每个副本都可以在另一个副本执行其他任务时,处理自己的任务;
2.一个进程想要执行另一个程序。

exec函数
将当前的进程映像替换成一个新的程序,进程ID不变。
6个exec函数的区别在于:
(1)待执行的程序文件由文件名还是路径名;
(2)新程序的参数是指针引用还是列出;
(3)把调用进程的环境给新程序还是给制定的环境给新程序
若调用成功不返回,失败返回-1
int execl(const char* pathname, const char* arg0,…);
int execv(const char* pathname, const char* argv[]);
int execle(const char* pathname, const char* arg0,…, char* const envp[]);
int execve(const char* pathname, const char* arg[], char* const envp[]);
int execlp(const char* filename, const char* arg0,…);
int execvp(const char* filename, const char* arg0,…);

close函数
int close(int sockfd);
成功返回0,出错返回-1
描述符引用计数:当前打开的引用该文件或套接字的描述符的个数,当调用fork函数之后子进程复制父进程的环境,是的引用计数由1变成2,因此调用close函数关闭父进程连接之后,引用计数由2减为1,这是子进程荏苒保持与客户端的连接,而父进程处理下一个客户端的连接。
若想要直接同时关闭父进程和子进程与客户端的连接,可以用shutdown函数替换close函数。

getsockname和getpeername函数
int getsockname(int sockfd, const struct sockaddr* localaddr, int len);返回与某个套接字关联的本地协议地址
int getpeername(int sockfd, const struct sockaddr* peeraddr, int len);返回与某个套接字关联的外地协议地址
成功返回0,失败返回-1.

回射服务器的步骤:
(1)客户从标准输入读入一个文本行,并写给服务器;
(2)服务器从网络中读取这段文本行,并回射给客户;
(3)客户从网络中读入这行文本,并显示在标准输入上。

POSIX信号处理总结:
1.一旦安装信号处理函数,它便一直安装着;
2.在一个信号处理函数运行期间,正被递交的信号是阻塞的。而且,安装处理函数时在传递给sigaction函数的sa_mask信号集中指定的任何额外信号也被阻塞。在书中的函数中,我们将sa_mask置为空,意味着除了被捕获的信号外,没有额外的信号被捕获;
3.如果一个信号在阻塞期间产生了一次或者多次,那么该信号在解阻塞之后通常只递交一次,就是说
UNIX函数是不排队的。
4.利用sigprocmask函数选择性地阻塞或解阻塞一组信号是有可能的

设置僵死状态的目的是维护子进程的信息(包括子进程的进程ID、终止状态、资源利用信息);当一个进程终止时,僵死状态的子进程的父进程的ID会被置为1(init process),然后init process会wait子进程,清理它们,解除子进程的僵死状态。

慢系统调用:适用于永远可能阻塞的系统调用。一般对管道和终端设备的读和写都属于慢系统调用。

wait和waitpid函数
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int statloc, int options);
成功返回进程ID,错误返回-1

wait和waitpid均返回两个值,已终止的进程的进程ID号以及通过statloc返回的子进程终止状态(一个整数);可调用三个宏来检查终止状态,分别为正常终止、有某个信号杀死或者由作业控制停止。
若调用wait的进程没有终止的子进程,但是有一个或者多个子进程在执行,那么将要阻塞到有某个子进程终止之后进行。
waitpid函数可以指定想要等待的子进程,值为-1表示等待第一个被终止的子进程,还可以控制其他属性。

SIGPIPE信号
当一个进程向某个已收到RST的套接字执行写操作时,内核会向该进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此进程必须捕获它以免被不情愿的终止。

终端命令:fuser -k 9877/tcp
-a 显示所有命令行中指定的文件,默认情况下被访问的文件才会被显示。
-c 和-m一样,用于POSIX兼容。
-k 杀掉访问文件的进程。如果没有指定-signal就会发送SIGKILL信号。
-i 杀掉进程之前询问用户,如果没有-k这个选项会被忽略。
-l 列出所有已知的信号名称。
-m name 指定一个挂载文件系统上的文件或者被挂载的块设备(名称name)。这样所有访问这个文件或者文件系统的进程都会被列出来。如果指定的是一个目录会自动转换成"name/",并使用所有挂载在那个目录下面的文件系统。
-n space 指定一个不同的命名空间(space).这里支持不同的空间文件(文件名,此处默认)、tcp(本地tcp端口)、udp(本地udp端口)。对于端口, 可以指定端口号或者名称,如果不会引起歧义那么可以使用简单表示的形式,例如:name/space (即形如:80/tcp之类的表示)。
-s 静默模式,这时候-u,-v会被忽略。-a不能和-s一起使用。
-signal 使用指定的信号,而不是用SIGKILL来杀掉进程。可以通过名称或者号码来表示信号(例如-HUP,-1),这个选项要和-k一起使用,否则会被忽略。
-u 在每个PID后面添加进程拥有者的用户名称。
-v 详细模式。输出似ps命令的输出,包含PID,USER,COMMAND等许多域,如果是内核访问的那么PID为kernel. -V 输出版本号。
-4 使用IPV4套接字,不能和-6一起应用,只在-n的tcp和udp的命名存在时不被忽略。
-6 使用IPV6套接字,不能和-4一起应用,只在-n的tcp和udp的命名存在时不被忽略。

  • 重置所有的选项,把信号设置为SIGKILL.

服务器主机崩溃
模拟实验:设置一个客户端和一个服务器(不在同一主机),客户端发送一条文本验证通信正常;切断服务器,客户端再发送一条文本给服务器。
结论:当服务器断开之后,客户端连接不上会尝试重连十二次,大约九分钟如果还是没有收到响应的话,则会给客户端发送一个ACK(ETIMEOUT),标识服务器已经崩溃;另一种方式可能在客户端与服务器之间存在路由器提前检测到客户端消息不可达或者客户端主机崩溃,那么路由器会给客户端发送一个destination unreachable的ERROR。
这种检测方法必须要给服务器发送消息才能判断出服务器是否崩溃。

服务器主机崩溃重启
模拟实验:设置一个客户端和一个服务器(不在同一主机),客户端发送一条文本验证通信正常;先建立连接,然后再从网络上断开服务器,实现将它关机后重启,最后连接到新的网络中。
结论:当服务器主机重启,之前连接的所有信息都被清空,重新建立新的连接,因此TCP服务器会给客户端发送一个RST,客户端收到RST之后会发生一个ECONNRESET错误。

服务器进程关机
服务器进程关机时,一般来说,init进程会先给所有进程发送一个SIGTERM,然后过一段固定时间(一般是15到20秒),会给所有进程发送SIGKILL信号,前一个信号可以被捕获,后一个不可以;若要想服务器进程一终止就被客户端进程捕获到,就需要调用select或poll函数。

服务器的本地端口由bind指定,两个外地地址,由accept返回给服务器;若服务器是在一个多宿主机上,那么使用getsockname确定IP地址;若另一个程序调用accept通过exec执行,那么需要调用getpeername来确定IP地址和端口号。

1.信号驱动I/O模型
首先要开启套接字的信号驱动I/O功能,并且通过sigaction系统调用安装一个信号处理函数。
这样既可以在信号处理函数中接收recvfrom通知,也可以通知主循环准备接收数据。

2.异步I/O模型
调用aio_read函数,这个函数给内核传递描述符、缓冲区指针、缓冲区大小、文件偏移,并通知内核在读写完成时如何通知。
这个系统调用立即返回并且在等待I/O期间不会阻塞进程。

信号驱动的I/O模型和异步I/O模型的主要区别:前者通知何时启动一个I/O操作而后者通知I/O操作何时完成。

同步I/O操作导致请求进程阻塞,直到I/O操作完成;异步I/O操作不导致进程阻塞

select函数
允许进程指示内核允许等待多个事件中的任何一个发生,并只在有一个或者多个事件发生或者经历一段时间之后才会唤醒。
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *excepset, const struct timeval *timeout);
若有就绪描述符则返回数目,超时返回0,出错返回-1.
struct timeval{
long tv_sec;
long tv_usec;
};// timeout参数表示内核等待一个买描述符就绪花费的时间,该参数可以为三个值,(1)不等待,值为0,设置为轮询;(2)一直等待,设置为空指针;(3)等待固定的时间。
const 表示这个时间不会被改变,不会因为返回值的变更而改变timeout的值。
maxfdp1 指定等待测试的描述符的个数
readset, writeset, excepset 这三个参数是让内核测试读、写、异常条件的描述符

描述符就绪的条件
满足下列条件,一个套接字准备好读
(1)该套接字的接收缓冲区的大小大于等于套接字接收缓冲区低水位标记的当前大小
(2)该连接的读半步关闭,接收到FIN的TCP连接
(3)该套接字为一个监听套接字且已连接的套接字数目不为0
(4)其上有一个套接字错误待处理
满足下列条件,一个套接字准备好写
(1)该套接字的发送缓冲区的大小大于等于套接字发送缓冲区低水位标记的当前大小
(2)该连接的写半步关闭,接收到FIN的TCP连接
(3)使用非阻塞connect套接字已建立连接
(4)其上有一个套接字错误待处理

接收缓冲区低水位标记和发送缓冲区低水位标记的目的在于:在应用进程允许select进行读写操作之前,有多少缓冲区可读可写。

由于fgets函数有缓冲区,而当fgets读到一行写给服务器之后就没有管在缓冲区中的数据而直接调用select执行新的服务,同理readline的自身的缓冲区会被select函数忽略,因此需要改善代码避免数据丢失。

shutdown函数
一般中止连接是用close函数,但是close函数会有两个弊端:1.调用close函数只会让描述符减1;2.close函数会关闭读和写双向的连接。
int shutdown(int sockfd, int howto);
howto的值决定函数的作用:
1.SHUT_WR:关闭写一半的套接字,缓冲区中的数据会发送结束;
2.SHUT_RD:关闭读一半的套接字,缓冲区中的数据会被丢掉;
3.SHUT_RDWR:连接的读和写都关闭。

拒绝服务型攻击:当服务器端连接多个客户端时,服务器因为某一个客户端阻塞而导致整个服务器进程阻塞,服务器处于挂起状态,不能为其他客户端服务。
解决办法:(1)设置非阻塞的I/O;(2)对I/O操作设置超时;(3)创建单独的线程进行服务。

pselect函数
int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *excepset, const struct timespec *timeout, const sigset_t *sigmask);
返回值有效返回的是描述符的数目,若超时返回0,若出错返回-1
struct timespec{
long tv_sec;
long tv_usec; //表示纳秒数
};

sigmask是一个指向信号掩码的指针,可以屏蔽某个中断信号,防止在阻塞的过程中被中断打断,造成错误。

poll函数
int poll(struct pollfd *fdarray, unsigned int nfds, int timeout);
若有就绪描述符则返回数目,超时返回0,出错返回-1
poll识别三类数据:普通、优先级带、高优先级。

poll函数对于TCP和UDP数据的识别性:
(1)所有正规的TCP和UDP的数据都被认定为普通数据
(2)TCP的带外数据被认定为是高优先级数据
(3)当TCP的读半部关闭时,被认定为普通数据
(4)TCP连接错误可以被认定为普通数据,也可以是错误
(5)在监听套接字上的新的连接可以输普通数据也可以是优先级的数据
(6)非阻塞式connect的完成被认定为使相应套接字可写

timeout:(1)不等待,值为0,设置为轮询;(2)一直等待,值为INFTIM,设置为空指针;(3)等待固定的时间。

混合使用stdio的缓冲机制和select的危险使得我们针对提供缓冲区,而不是使用文本行操作的客户程序和服务器程序的正确版本。

套接字选项
函数:
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
level:指定系统中某个特定协议的代码,某个通用套接字代码,或者解释选项的代码(比如:IPV4,IPV6,TCP或者SCTP)
optval是指向某个变量的指针,在set中指向的是等待设置的新的套接字选项,在get中指向的是已经获取的套接字的值。
套接字选项分为两个类型,一是启动或者禁止某个特定的二元选项;二是取得并且返回已经设置的套接字选项。

accept连接成功会返回已连接套接字,该套接字选项是在三路握手连接成功连接套接字设置进去的。

通用套接字选项与协议无关,但是有些套接字选项只能应用到特定的类型中。比如SO_BROADCAST套接字应用于数据包套接字。

SO_BROADCAST套接字
只支持在支持广播的网络上的数据包套接字设置这一选项。用于防止不支持广播的网络设置了广播地址。

SO_DEBUG套接字
只支持TCP套接字,用于记录套接字发送和接收的分组详细信息。

SO_DONTROUTE套接字
用于绕过正常的路由机制的,定向到固定的网络地址或者本地的接口的套接字

SO_ERROR套接字
当一个套接字发生错误时,通知进程错误的套接字。read或者write返回错误时,若SO_ERROR套接字不为0,则返回-1,并且errorno为SO_ERROR套接字的值。或者套接字阻塞在select上面,select均返回并且设置一个或者全部两个状态

SO_KEEPALIVE 主要功能:检测对面主机是否崩溃或者不可达
TCP设置存活选项之后,会在两小时内没有收到任何消息的情况下给对端发送一个探测存活的分节,收到对端的响应有三种情况:(1)收到ACK的响应;(2)收到RST响应;(3)没有收到任何响应。

SO_LINGER
指定close函数对面向协议的套接字如何进行操作。
Struct linger {
int l_onoff; //on表示开启选项
int l_linger; //linger time 表示套接字关闭时,进程会进入睡眠,知道缓冲区中所有的数据发送完毕或者对端进程的延滞时间到。

设置SO_LINGER选项之后,close的返回只能说明套接字被确认,并不代表被服务器读取。如果要知道服务器读取的话,需要使用shutdown函数来关闭套接字,read会阻塞等待服务进程读取套接字。

SO_SNDBUF和SO_RCVBUF
这两个套接字允许改变缓冲区的大小。由于TCP的窗口规模选项是建立连接时用SYN套接字得到的,因此必须在监听套接字选项时设置SO_RCVBUF。
TCP套接字接收缓冲区必须至少是MSS的四倍,原因是TCP的快速恢复算法,发送端会发三个重复的分节验证是否存在分节。

传输数据的管道容量的单位为带宽(bit/s)-延迟积,是带宽和RTT的相乘的结果,管道容量越大,相应的接收缓冲区的数量也要变大。

猜你喜欢

转载自blog.csdn.net/weixin_34764432/article/details/84929158