咱们知道进程通讯的方法有管道、命名管道、信号、消息队列、共享内存、信号量,这些方法都要求通讯的两个进程位于同一个主机。可是若是通讯双方不在同一个主机又该如何进行通讯呢?在计算机网络中咱们就学过了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
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
咱们知道具体的一个文件能够用一个路径来表示,好比/home/zzy/src_code/client.c,那么具体的socket实例咱们该如何表示呢,其实就是使用上面提到的so_pcb的那几个关键属性,也就是使用so_type+ip地址+端口号。若是咱们使用so_type+ip地址+端口号实例一个socket,那么互联网上的其余主机就能够与该socket实例进行通讯了。因此下面咱们看一下socket如何进行实例化,看看socket给咱们提供了哪些接口,而咱们又该如何组织这些接口tcp
int socket(int protofamily, int so_type, int protocol);
这里解释一下图三,图三实际上是使用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
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind函数就是给图三种so_pcb结构中的地址赋值的接口
struct sockaddr实际上是void的typedef,其常见的结构以下图(图片来源传智播客邢文鹏linux系统编程的笔记),这也是为何须要addrlen参数的缘由,不一样的地址类型,其地址长度不同:
图5 地址结构图
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 */ };
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 */ };
#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这三个参数和bind的三个参数类型一直,只不过此处strcut sockaddr表示对端公开的地址。三个参数都是传入参数。connect顾名思义就是拿来创建链接的函数,只有像tcp这样面向链接、提供可靠服务的协议才须要创建链接
int listen(int sockfd, int backlog)
告知内核在sockfd这个描述符上监听是否有链接到来,并设置同时能完成的最大链接数为backlog。3.6节还会继续解释这个参数。当调用listen后,内核就会创建两个队列,一个SYN队列,表示接受到请求,但未完成三次握手的链接;另外一个是ACCEPT队列,表示已经完成了三次握手的队列
关于backlog , man listen的描述以下:
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的这两个属性被初始化了。
以AF_INET,SOCK_STREAM,IPPROTO_TCP三个参数实例化的socket为例,经过一个副图来说解这三个函数的工做流程及粗浅原理(图片改自http://blog.csdn.net/russell_tao/article/details/9111769)
图6 listen、accept、connect流程及原理图
这就是listen,accept,connect这三个函数的工做流程及原理。从这个过程能够看到,在connect函数中发生了两次握手。
更加详细的accept创建链接流程及原理请移步下面这个博文,该博文博主是个大牛,讲解的通熟易懂而且有深度:
http://blog.csdn.net/russell_tao/article/details/9111769
#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:
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)).
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.
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.
#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编程的通常模型是固定的,下面我就以几幅图来讲明,因为插图中已经有说明,我就不在作补充说明了。
图8 c/s模型tcp编程流程图及tcp状态变迁图
图9 c/s模型udp编程流程图
参考资料:
《tcp/ip协议详解卷1、卷二》