socket原理详解

一、什么是socket

咱们知道进程通讯的方法有管道、命名管道、信号、消息队列、共享内存、信号量,这些方法都要求通讯的两个进程位于同一个主机。可是若是通讯双方不在同一个主机又该如何进行通讯呢?在计算机网络中咱们就学过了tcp/ip协议族,其实使用tcp/ip协议族就能达到咱们想要的效果,以下图(图片来源于《tcp/ip协议详解卷一》第一章1.3)linux

         程序员

                           图一 各协议所处层次编程

固然,这样作当然是能够的,可是,当咱们使用不一样的协议进行通讯时就得使用不一样的接口,还得处理不一样协议的各类细节,这就增长了开发的难度,软件也不易于扩展。因而UNIX BSD就发明了socket这种东西,socket屏蔽了各个协议的通讯细节,使得程序员无需关注协议自己,直接使用socket提供的接口来进行互联的不一样主机间的进程的通讯。这就比如操做系统给咱们提供了使用底层硬件功能的系统调用,经过系统调用咱们能够方便的使用磁盘(文件操做),使用内存,而无需本身去进行磁盘读写,内存管理。socket其实也是同样的东西,就是提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就能够统1、方便的使用tcp/ip协议的功能了。百说不如一图,看下面这个图就能明白了。服务器

         

                               图二 socket所处层次cookie

那么,在BSD UNIX又是如何实现这层抽象的呢?咱们知道unix中万物皆文件,没错,bsd在实现上把socket设计成一种文件,而后经过虚拟文件系统的操做接口就能够访问socket,而访问socket时会调用相应的驱动程序,从而也就是使用底层协议进行通讯。(vsf也就是unix提供给咱们的面向对象编程,若是底层设备是磁盘,就对磁盘读写,若是底层设备是socket就使用底层协议在网中进行通讯,而对外的接口都是一致的)。下面再看一下socket的结构是怎样的(图片来源于《tcp/ip协议详解卷二》章节一,1.8描述符),注意:这里的socket是一个实例化以后的socket,也就是说是一个具体的通讯过程当中的socket,不是指抽象的socket结构,下文还会进行解释。网络

         

                          图三 udp socket实例的结构并发

很明显,unix把socket设计成文件,经过描述符咱们能够定位到具体的file结构体,file结构体中有个f_type属性,标识了文件的类型,如图,DTYPE_VNODE表示普通的文件DTYPE_SOCKET表示socket,固然还有其余的类型,好比管道、设备等,这里咱们只关心socket类型。若是是socket类型,那么f_ops域指向的就是相应的socket类型的驱动,而f_data域指向了具体的socket结构体,socket结构体关键域有so_type,so_pcb。so_type常见的值有:dom

  • SOCK_STREAM 提供有序的、可靠的、双向的和基于链接的字节流服务,当使用Internet地址族时使用TCP。
  • SOCK_DGRAM 支持无链接的、不可靠的和使用固定大小(一般很小)缓冲区的数据报服务,当使用Internet地址族使用UDP。
  • SOCK_RAW 原始套接字,容许对底层协议如IP或ICMP进行直接访问,能够用于自定义协议的开发。

so_pcb表示socket控制块,其又指向一个结构体,该结构体包含了当前主机的ip地址(inp_laddr),当前主机进程的端口号(inp_lport),发送端主机的ip地址(inp_faddr),发送端主体进程的端口号(inp_fport)。so_pcb是socket类型的关键结构,不亚于进程控制块之于进程,在进程中,一个pcb能够表示一个进程,描述了进程的全部信息,每一个进程有惟一的进程编号,该编号就对应pcb;socket也同时是这样,每一个socket有一个so_pcb,描述了该socket的全部信息,而每一个socket有一个编号,这个编号就是socket描述符。说到这里,咱们发现,socket确实和进程很像,就像咱们把具体的进程当作是程序的一个实例,一样咱们也能够把具体的socket当作是网络通讯的一个实例。socket

二、具体socket实例如何标识

咱们知道具体的一个文件能够用一个路径来表示,好比/home/zzy/src_code/client.c,那么具体的socket实例咱们该如何表示呢,其实就是使用上面提到的so_pcb的那几个关键属性,也就是使用so_type+ip地址+端口号。若是咱们使用so_type+ip地址+端口号实例一个socket,那么互联网上的其余主机就能够与该socket实例进行通讯了。因此下面咱们看一下socket如何进行实例化,看看socket给咱们提供了哪些接口,而咱们又该如何组织这些接口tcp

三、socket编程接口

3.一、socket接口

int socket(int protofamily, int so_type, int protocol);

  • protofamily 指协议族,常见的值有:
    AF_INET,指定so_pcb中的地址要采用ipv4地址类型
    AF_INET6,指定so_pcb中的地址要采用ipv6的地址类型
    AF_LOCAL/AF_UNIX,指定so_pcb中的地址要使用绝对路径名
    固然也还有其余的协议族,用到再学习了
  • so_type 指定socket的类型,也就是上面讲到的so_type字段,比较经常使用的类型有:
    SOCK_STREAM
    SOCK_DGRAM
    SOCK_RAW
  • protocol 指定具体的协议,也就是指定本次通讯能接受的数据包的类型和发送数据包的类型,常见的值有:
    IPPROTO_TCP,TCP协议
    IPPROTO_UDP,UPD协议
    0,若是指定为0,表示由内核根据so_type指定默认的通讯协议

这里解释一下图三,图三实际上是使用AF_INET,SOCK_DGRAM,IPPRTO_UDP实例化以后的一个具体的socket。

那为何要经过这三个参数来生成一个socket描述符?

答案就是经过这三个参数来肯定一组固定的操做。咱们说过抽象的socket对外提供了一个统1、方便的接口来进行网络通讯,但对内核来讲,每个接口背后都是及其复杂的,同一个接口对应了不一样协议,而内核有不一样的实现,幸运的是,若是肯定了这三个参数,那么相应的接口的映射也就肯定了。在实现上,BSD就把socket分类描述,每个类别都有进行通讯的详细操做,分类见下图。而对socket的分类,就比如对unix设备的分类,咱们对设备write和read时,底层的驱动是有各个设备本身提供的,而socket也同样,当咱们指定不一样的so_type时,底层提供的通讯细节也由相应的类别提供。

                  

                                  图4 socket层次图

更详细的socket()函数参数描述请移步:
http://blog.csdn.net/liuxingen/article/details/44995467

http://blog.csdn.net/qiuchangyong/article/details/50099927

3.二、bind接口

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

bind函数就是给图三种so_pcb结构中的地址赋值的接口

  • sockfd   是调用socket()函数建立的socket描述符
  • addr     是具体的地址
  • addrlen  表示addr的长度

struct sockaddr实际上是void的typedef,其常见的结构以下图(图片来源传智播客邢文鹏linux系统编程的笔记),这也是为何须要addrlen参数的缘由,不一样的地址类型,其地址长度不同:

                  

                              图5 地址结构图

  • AF_INET:
struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};
  • AF_INET6:
struct sockaddr_in6 { 
    sa_family_t     sin6_family;   /* AF_INET6 */ 
    in_port_t       sin6_port;     /* port number */ 
    uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
    struct in6_addr sin6_addr;     /* IPv6 address */ 
    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};
struct in6_addr { 
    unsigned char   s6_addr[16];   /* IPv6 address */ 
};
  • AF_UNIX:
#define UNIX_PATH_MAX    108
struct sockaddr_un { 
    sa_family_t sun_family;               /* AF_UNIX */ 
    char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};

3.三、connect接口

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

这三个参数和bind的三个参数类型一直,只不过此处strcut sockaddr表示对端公开的地址。三个参数都是传入参数。connect顾名思义就是拿来创建链接的函数,只有像tcp这样面向链接、提供可靠服务的协议才须要创建链接

 

3.四、listen接口

int listen(int sockfd, int backlog)

告知内核在sockfd这个描述符上监听是否有链接到来,并设置同时能完成的最大链接数为backlog。3.6节还会继续解释这个参数。当调用listen后,内核就会创建两个队列,一个SYN队列,表示接受到请求,但未完成三次握手的链接;另外一个是ACCEPT队列,表示已经完成了三次握手的队列

  • sockfd 是调用socket()函数建立的socket描述符
  • backlog 已经完成三次握手而等待accept的链接数

关于backlog , man listen的描述以下:

  • The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information.
  • If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128.

3.五、accept接口

int accept(int listen_sockfd, struct sockaddr *addr, socklen_t *addrlen)

这三个参数与bind的三个参数含义一致,不过,此处的后两个参数是传出参数。在使用listen函数告知内核监听的描述符后,内核就会创建两个队列,一个SYN队列,表示接受到请求,但未完成三次握手的链接;另外一个是ACCEPT队列,表示已经完成了三次握手的队列。而accept函数就是从ACCEPT队列中拿一个链接,并生成一个新的描述符,新的描述符所指向的结构体so_pcb中的请求端ip地址、请求端端口将被初始化。

从上面能够知道,accpet的返回值是一个新的描述符,咱们姑且称之为new_sockfd。那么new_sockfd和listen_sockfd有和不一样呢?不一样之处就在于listen_sockfd所指向的结构体so_pcb中的请求端ip地址、请求端端口没有被初始化,而new_sockfd的这两个属性被初始化了。

3.六、listen、connect、accept流程及原理

以AF_INET,SOCK_STREAM,IPPROTO_TCP三个参数实例化的socket为例,经过一个副图来说解这三个函数的工做流程及粗浅原理(图片改自http://blog.csdn.net/russell_tao/article/details/9111769

       

                           图6 listen、accept、connect流程及原理图

  1. 服务器端在调用listen以后,内核会创建两个队列,SYN队列和ACCEPT队列,其中ACCPET队列的长度由backlog指定。
  2. 服务器端在调用accpet以后,将阻塞,等待ACCPT队列有元素。
  3. 客户端在调用connect以后,将开始发起SYN请求,请求与服务器创建链接,此时称为第一次握手。
  4. 服务器端在接受到SYN请求以后,把请求方放入SYN队列中,并给客户端回复一个确认帧ACK,此帧还会携带一个请求与客户端创建链接的请求标志,也就是SYN,这称为第二次握手
  5. 客户端收到SYN+ACK帧后,connect返回,并发送确认创建链接帧ACK给服务器端。这称为第三次握手
  6. 服务器端收到ACK帧后,会把请求方从SYN队列中移出,放至ACCEPT队列中,而accept函数也等到了本身的资源,从阻塞中唤醒,从ACCEPT队列中取出请求方,从新创建一个新的sockfd,并返回。

这就是listen,accept,connect这三个函数的工做流程及原理。从这个过程能够看到,在connect函数中发生了两次握手。

更加详细的accept创建链接流程及原理请移步下面这个博文,该博文博主是个大牛,讲解的通熟易懂而且有深度:

http://blog.csdn.net/russell_tao/article/details/9111769

3.七、发送消息接口

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

这几个接口都比较好理解,查一下man pages就知道什么含义了,man pages中讲解的很是清楚。这里只说一下flags参数,也是摘抄自man pages。

flags:

  • MSG_DONTWAIT (since Linux 2.2),不阻塞

Enables nonblocking operation; if the operation would block, EAGAIN or EWOULDBLOCK is returned (this can also be enabled using
the O_NONBLOCK flag with the F_SETFL fcntl(2)).

  • MSG_DONTROUTE,数据包不容许经过网关

Don't use a gateway to send out the packet, only send to hosts on directly connected networks. This is usually used only by
diagnostic or routing programs. This is only defined for protocol families that route; packet sockets don't.

  • MSG_OOB,带外数据

Sends out-of-band data on sockets that support this notion (e.g., of type SOCK_STREAM); the underlying protocol must also sup‐
port out-of-band data.

  • 其余

3.八、接受消息接口

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

这几个接口都比较好理解,查一下man pages就知道什么含义了,man pages中讲解的很是清楚。

四、socket编程流程及tcp状态变迁

先作一个说明,下面的图都不是原创,是本人收藏已久的一些原理图,来源已经不记得了,若是你们知道来源的能够留言。

socket编程的通常模型是固定的,下面我就以几幅图来讲明,因为插图中已经有说明,我就不在作补充说明了。

4.1 c/s模式之TCP

 

 

                             图8 c/s模型tcp编程流程图及tcp状态变迁图

4.2 c/s模式之UDP

                                图9 c/s模型udp编程流程图

 

参考资料:

《tcp/ip协议详解卷1、卷二》

socket函数的domain、type、protocol解析

建立socket函数的第三个参数的意义

陶辉:高性能网络编程(一)----accept创建链接

什么是带外数据

Linux的SOCKET编程详解

相关文章
相关标签/搜索