linux - 套接字编程

套接字编程

套接字,也叫socket,是操作系统内核中的一个数据结构,它是网络中的节点进行相互通信的门户。网络通信,说白了就是进程间的通信(同一台机器上不同进程或者不同计算机上的进程间通信)。

在网络中,每一台计算机或者路由都有一个网络地址,就是IP地址。两个进程通信时,首先要确定各自所在的网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上一般都是同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中的哪一个进程进行通信,因此套接口中还需要包括其他的信息,比如端口号和协议。

socket函数

为了执行网络I\O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议;

#include <sys/socket.h>
int socket(int family, int type, int protocol);

其中family指定协议族,type标识套接字类型,protocol标识某个协议类型常值,通常为0;
linux支持一个新的套接字类型SOCK_PACKET
在这里插入图片描述

socket函数中family和type参数的组合:
在这里插入图片描述

connect函数

TCP客户用connect函数来建立与TCP服务器的连接;

#include <sys/types.h>          /* See NOTES */

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);

参数列表:
sockfd是由socket函数返回的套接字描述符
addr指向套接字地址结构的指针
addrlen代表该结构的大小
套接字地址结构必须含有服务器的IP地址和端口

bind函数

bind函数把一个本地协议地址赋予一个套接字;

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,         socklen_t addrlen);

参数列表:
sockfd是由socket函数返回的套接字描述符
addr是一个指向特定于协议的地址结构的指针
addrlen代表该结构的大小

扫描二维码关注公众号,回复: 12422523 查看本文章

服务器在启动时绑定的端口,如果一个客户端或服务器未曾调用bind绑定一个端口,当调用listen或者connect时,内核将为其选择一个临时端口;

进程可以把一个特定的IP地址绑定在它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一。对于TCP客户端,这就为在该套接字上发送的IP数据报指定了源IP地址;对于TCP服务端,这就限定该套接字只接受那些目的地为这个IP地址的客户链接;
在这里插入图片描述
如果我们指定的端口号为0,那么内核就会为其分配一个临时端口,如果指定ip地址为通配地址(127.0.0.1),内核将等到其套接字已经连接或已在套接字上发送数据包时才选择一个地址;

listen

listen仅由服务端调用,它完成两项工作:

  • 当socket创建一个套接字的时候,它被假设顶为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户桃子姐。listen将一个未连接的套接字转换为被动套接字,指示内核应当接受指向该套接字的连接请求;
  • 规定了内核应当为相应套接字排队的最大数量,也就是最多允许多少个连接;
#include <sys/types.h>          /* See NOTES */

#include <sys/socket.h>

int listen(int sockfd, int backlog);
  

listen函数应该在socket和bind函数之后,并在调用accept之前;
为了足够理解backlog参数,我们需要认识到内核为任何一个给定的监听套接字维护两个队列:

  • 未完成队列,每个这样的SYN分节对应其中一项,已由某个客户发出并到达服务器,而服务器在等待完成的TCP三次握手,这些套接字处于SYN_RCVD状态
  • 已完成队列,每个已完成TCP三次握手过程的客户对应其中一项;
    在这里插入图片描述

每当未完成队列中创建一项的时候,来自监听套接字的参数就立即复制到即将建立的队列中。
在这里插入图片描述
关于这两个队列,需要考虑的如下:

  • listen函数的backlog参数曾被定义为这两个队列的总和的最大值
  • 不要把backlog设置为0,因为不同的实现对此有不同的解释。如果你不需要客户连接,关闭该套接字即可
  • 在三次握手正常的情况下,未完成连接的队列中的任何一项在其中的留存时间是一个RTT,而RTT的值取决于特定的客户与服务器;
  • 在完成三次握手后,但在服务器调用accept之前到达的数据应该由服务端排队,最大数据量为相应连接套接字的接收缓冲区大小;

accept函数

accept由服务端调用,用于从已连接的队列头返回下一个已完成的连接,如果已完成的队列为空,那么该进程被投入睡眠;

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <sys/socket.h>

int accept4(int sockfd, struct sockaddr *addr,
            socklen_t *addrlen, int flags);

参数列表:
sockfd为已连接套接字描述符;
addr为客户端返回的协议地址
addrlen为该地址的大小

参数cliaddr和addrlen用于返回已连接的客户端的协议地址;
调用前,我们将由*addrlen所引用的整数值为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数;

如果accept成功,那么它的返回值是由内核生成的一个全新的描述符,代表所返回客户的TCP连接。

如果我们对客户端返回的值不关系,那么将后两个参数置为空

fork和exec

fork函数我们不再赘述,它的主要作用就是创建子进程,这也是linux唯一派生新进程的方法;
fork函数比较难理解的点在于它调用后会返回两个值,它在调用进程的时候返回一次,返回值是子进程的PID,在子进程中返回一次,值为0;

fork在子进程返回0而不是附近中的ID在于,一个父进程可以拥有多个子进程,子进程可以通过getppid获取到父进程的pid,相反父进程无法通过函数获取到子进程的pid,如果父进程需要记录子进程的pid,那么只有记录每次fork的返回值;

fork典型用法:

  • 一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行的时候处理各自的操作,这是网络服务器的经典用法;
  • 一个进程想要执行另一个程序,在linux中唯一的方法就是fork,该进程首先调用fork创建一个自身,然后再去另一个子进程中调用exec族函数,exec函数将当前进程映像替换成新的程序,这也是shell的经典写法;

任何一个存放在硬盘上的代码想要被执行,由一个现有的进程调用六个exec族函数中的一个,exec将当前进程映像替换成新的程序文件,而新的程序文件是从main开始执行。进程的PID并不会发生改变,我们成exec的进程为调用程序,并且执行新的程序;

#include <unistd.h> 
extern char **environ; 

int execl(const char *path, const char *arg, ...); 
int execlp(const char *file, const char *arg, ...); 
int execle(const char *path, const char *arg, ..., char * const envp[]); 
int execv(const char *path, char *const argv[]); 
int execvp(const char *file, char *const argv[]); 
int execvpe(const char *file, char *const argv[], 
char *const envp[]); 

这些函数只有调用出错才会回到子进程,否则,控制权将会交给新程序的起点,通常为main函数;

在这里插入图片描述

并发服务器

unix中编写并发服务器程序最简单的办法就是fork一个子进程来服务每个客户;
当一个客户连接的时候,accept返回,服务器接着fork,然后由子进程通过已连接套接字,父进程则等待一个连接,既然新的客户端子进程提供服务,父进程关闭已连接套接字;

在这里插入图片描述
从accept返回后,连接被内核接受,新的套接字connfd被创建,这是一个已经连接的套接字,可由此跨连接读取数据;
在这里插入图片描述

并发服务器下一步调用fork;
在这里插入图片描述

此时listenfd喝connfd这两个描述符都在父子进程间共享;再下一步是由父进程关闭已连接的套接字;
在这里插入图片描述
这是两个套接字所期望的终极状态,子进程处理与客户端的连接,父进程则可以再监听套接字上次调用accept来处理下一个客户的连接;

close函数

通常unix close函数也用来关闭套接字,也可以关闭文件描述符,并且终止TCP连接;

#include <unistd.h>
int close(int sockfd);

描述符引用计数:
如果父进程对每个由accept返回的已连接套接字都不close,那么并发服务器最终会耗尽可用的描述符,因为任何进程再任何时刻都拥有打开着的描述符是有限制的。不过更重要的是,没有客户端去终止,那么它的引用计数一直都是1,浙江妨碍TCP连接终止序列的发生;

getsockname和getperrname

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);   

这两个函数返回与某个套接字关联的本地协议,或者返回某个与套接字关联的外地地址协议;

  • 在一个没有调用bind的TCP客户端上,connect成功后,getsockname用于返回由内核赋予该连接的本地地址和本地端口号;
  • 在以端口号0调用bind后,getsockname用于返回由内核赋予的本地端口号;
  • getsockname可用于获取某个套接字的协议族;
  • 在一个以统配IP地址调用的TCP服务器上,在某个客户的连接一旦(accept)后,getsockname就可以返回由内核赋予该链接的本地IP地址。
  • 当一个服务器是由调用锅accept的某个进程通过调用exec执行程序时,它能获取客户身份的唯一途径便是调用getperrname。
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43791961/article/details/111124544