我们讲求信息交流的价值,那网路中进程之间怎样通讯,如我们每晚打开浏览器浏览网页时,浏览器的进程如何与web服务器通讯的?当你用QQ聊天时,QQ进程如何与服务器或你好友所在的QQ进程通讯?那些都得靠socket?那哪些是socket?socket的类型有什么?还有socket的基本函数,那些都是本文想介绍的。
本文的主要内容如下:
1、网络中进程之间怎样通讯?
本地的进程间通讯(IPC)有好多种方法,但可以总结为下边4类:
但那些都不是本文的主题!我们要讨论的是网路中进程之间怎样通讯?首要解决的问题是怎样惟一标示一个进程,否则通讯无从谈起!在本地可以通过进程PID来惟一标示一个进程,而且在网路中这是行不通的。虽然TCP/IP合同族早已帮我们解决了这个问题,网路层的“ip地址”可以惟一标示网路中的主机,而传输层的“协议+端口”可以惟一标示主机中的应用程序(进程)。这样借助三元组(ip地址,合同,端口)就可以标示网路的进程了,网路中的进程通讯就可以借助这个标志与其它进程进行交互。
使用TCP/IP合同的应用程序一般采用应用编程插口:UNIXBSD的套接字(socket)和UNIXSystemV的TLI(早已被淘汰),来实现网路进程之间的通讯。就目前而言,几乎所有的应用程序都是采用socket,而如今又是网路时代,网路中进程通讯是无处不在,这就是我为何说“一切皆socket”。
2、什么是Socket?
前面我们早已晓得网路中的进程是通过socket来通讯的,那哪些是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open–>读写write/read–>关掉close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这种函数我们在旁边进行介绍。
socket一词的起源
在组网领域的首次使用是在1970年2月12日发布的文献IETFRFC33中发觉的,撰写者为StephenCarr、SteveCrocker和VintCerf。按照英国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字插口。一个套接字插口构成一个联接的一端,而一个联接可完全由一对套接字插口规定。”计算机历史博物馆补充道:“这比BSD的套接字插口定义早了大概12年。”
3、socket的基本操作
既然socket是“open—write/read—close”模式的一种实现,这么socket就提供了这种操作对应的函数插口。下边以TCP为例,介绍几个基本的socket插口函数。
3.1、socket()函数
intsocket(intdomain,inttype,intprotocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socketdescriptor),它惟一标示一个socket。这个socket描述字跟文件描述字一样unix网络编程:套接字联网api,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时侯,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
注意:并不是前面的type和protocol可以随便组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会手动选择type类型对应的默认合同。
当我们调用socket创建一个socket时,返回的socket描述字它存在于合同族(addressfamily,AF_XXX)空间中,但没有一个具体的地址。假如想要给它形参一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会手动随机分配一个端口。
3.2、bind()函数
正如前面所说bind()函数把一个地址族中的特定地址赋给socket。诸如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端标语组合赋给socket。
intbind(intsockfd,conststructsockaddr*addr,socklen_taddrlen);
函数的三个参数分别为:
一般服务器在启动的时侯就会绑定一个众所周知的地址(如ip地址+端标语),用于提供服务,顾客就可以通过它来接连服务器;而顾客端就不用指定,有系统手动分配一个端标语和自身的ip地址组合。这就是为何一般服务器端在listen之前会调用bind(),而顾客端就不会调用,而是在connect()时由系统随机生成一个。
网路字节序与主机字节序
主机字节序就是我们平时说的大端和小端模式:不同的CPU有不同的字节序类型,这种字节序是指整数在显存中保存的次序,这个称作主机序。引用标准的Big-Endian和Little-Endian的定义如下:
a)Little-Endian就是高位字节排放到显存的低地址端unix网络编程:套接字联网api,低位字节排放到显存的高地址端。
b)Big-Endian就是低位字节排放到显存的低地址端,高位字节排放到显存的高地址端。
网路字节序:4个字节的32bit值以下边的顺序传输:首先是0~7bit,其次8~15bit,之后16~23bit,最后是24~31bit。这些传输顺序叫做大端字节序。因为TCP/IP首部中所有的二补码整数在网路中传输时都要求以这些顺序,因而它又叫做网路字节序。字节序,顾名思义字节的次序,就是小于一个字节类型的数据在显存中的储存次序,一个字节的数据没有次序的问题了。
所以:在将一个地址绑定到socket的时侯,请先将主机字节序转换成为网路字节序,而不要假设主机字节序跟网路字节序一样使用的是Big-Endian。因为这个问题曾引起过骚乱!公司项目代码中因为存在这个问题,引起了好多莫名其妙的问题,所以请紧记对主机字节序不要做任何假设,勿必将其转化为网路字节序再赋给socket。
3.3、listen()、connect()函数
假如作为一个服务器,在调用socket()、bind()以后还会调用listen()来窃听这个socket,假如顾客端这时调用connect()发出联接恳求,服务器端都会接收到这个恳求。
intlisten(intsockfd,intbacklog);intconnect(intsockfd,conststructsockaddr*addr,socklen_taddrlen);
listen函数的第一个参数即为要窃听的socket描述字,第二个参数为相应socket可以排队的最大联接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待顾客的联接恳求。
connect函数的第一个参数即为顾客端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的厚度。顾客端通过调用connect函数来构建与TCP服务器的联接。
3.4、accept()函数
TCP服务器端依次调用socket()、bind()、listen()以后,都会窃听指定的socket地址了。TCP顾客端依次调用socket()、connect()以后就想TCP服务器发送了一个联接恳求。TCP服务器窃听到这个恳求以后,都会调用accept()函数取接收恳求,这样联接就完善好了。以后就可以开始网路I/O操作了,即类同于普通文件的读写I/O操作。
intaccept(intsockfd,structsockaddr*addr,socklen_t*addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向structsockaddr*的表针,用于返回顾客端的合同地址,第三个参数为合同地址的厚度。假如accpet成功,这么其返回值是由内核手动生成的一个全新的描述字,代表与返回顾客的TCP联接。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为窃听socket描述字;而accept函数返回的是已联接的socket描述字。一个服务器一般一般仅仅只创建一个窃听socket描述字,它在该服务器的生命周期内始终存在。内核为每位由服务器进程接受的顾客联接创建了一个已联接socket描述字,当服务器完成了对某个顾客的服务,相应的已联接socket描述字就被关掉。
3.5、read()、write()等函数
万事具备只欠东风,至此服务器与顾客早已构建好联接了。可以调用网路I/O进行读写操作了,即实现了网咯中不同进程之间的通讯!网路I/O操作有下边几组:
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把里面的其它函数都替换成这两个函数。它们的申明如下:
#includessize_tread(intfd,void*buf,size_tcount);ssize_twrite(intfd,constvoid*buf,size_tcount);#include#includessize_tsend(intsockfd,constvoid*buf,size_tlen,intflags);ssize_trecv(intsockfd,void*buf,size_tlen,intflags);ssize_tsendto(intsockfd,constvoid*buf,size_tlen,intflags,conststructsockaddr*dest_addr,socklen_taddrlen);ssize_trecvfrom(intsockfd,void*buf,size_tlen,intflags,structsockaddr*src_addr,socklen_t*addrlen);ssize_tsendmsg(intsockfd,conststructmsghdr*msg,intflags);ssize_trecvmsg(intsockfd,structmsghdr*msg,intflags);
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,假如返回的值是0表示早已读到文件的结束了,大于0表示出现了错误。倘若错误为EINTR说明读是由中断造成的,假如是ECONNREST表示网路联接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。在网路程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值小于0,表示写了部份或则是全部的数据。2)返回的值大于0,此时出现了错误。我们要依照错误类型来处理。倘若错误为EINTR表示在写的时侯出现了中断错误。倘若为EPIPE表示网路联接出现了问题(对方早已关掉了联接)。
其它的我就不一一介绍这几对I/O函数了,具体参见man文档或则baidu、Google,下边的事例上将使用到send/recv。
3.6、close()函数
在服务器与顾客端构建联接以后,会进行一些读写操作,完成了读写操作就要关掉相应的socket描述字,好比操作完打开的文件要调用fclose关掉打开的文件。
#includeintclose(intfd);
close一个TCPsocket的缺省行为时把该socket标记为以关掉,之后立刻返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时侯,就会触发TCP顾客端向服务器发送中止联接恳求。
4、socket中TCP的三次握手构建联接解读
我们晓得tcp构建联接要进行“三次握手”,即交换三个分组。大致流程如下:
只有就完了三次握手,并且这个三次握手发生在socket的那几个函数中呢?请看右图:
图1、socket中发送的TCP三次握手
从图中可以看出,当顾客端调用connect时,触发了联接恳求,向服务器发送了SYNJ包,这时connect步入阻塞状态;服务器窃听到联接恳求,即收到SYNJ包,调用accept函数接收恳求向顾客端发送SYNK,ACKJ+1,这时accept步入阻塞状态;顾客端收到服务器的SYNK,ACKJ+1以后,这时connect返回,并对SYNK进行确认;服务器收到ACKK+1时,accept返回,至此三次握手完毕,联接构建。
总结:顾客端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。
5、socket中TCP的四次握手释放联接解读
里面介绍了socket中TCP的三次握手构建过程,及其涉及的socket函数。如今我们介绍socket中的四次握手释放联接的过程,请看右图:
图2、socket中发送的TCP四次握手
图示过程如下:
这样每位方向上都有一个FIN和ACK。
6.下边给出实现的一个实例
首先,先给出实现的截图
服务器端代码如下:
#include"InitSock.h"
#include
#include
usingnamespacestd;
CInitSockinitSock;//初始化Winsock库
intmain()
//创建套节字
SOCKETsListen=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//拿来指定套接字使用的地址格式,一般使用AF_INET
//指定套接字的类型,若是SOCK_DGRAMlinux虚拟机,则用的是udp不可靠传输
//配合type参数使用,指定使用的合同类型(当指定套接字类型后,可以设置为0,由于默认为UDP或TCP)
if(sListen==INVALID_SOCKET)
printf("Failedsocket()n");
return0;
//填充sockaddr_in结构,是个结构体
/*structsockaddr_in{
shortsin_family;//地址族(指定地址格式),设为AF_INET
u_shortsin_port;//端标语
structin_addrsin_addr;//IP地址
charsin_zero[8];//空子节,设为空
}*/
sockaddr_insin;
sin.sin_family=AF_INET;
sin.sin_port=htons(4567);//1024~49151:普通用户注册的端标语
sin.sin_addr.S_un.S_addr=INADDR_ANY;
//绑定这个套节字到一个本地地址
if(::bind(sListen,(LPSOCKADDR)&sin,sizeof(sin))==SOCKET_ERROR)
printf("Failedbind()n");
return0;
//步入窃听模式
//2指的是,窃听队列中容许保持的仍未处理的最大联接数
if(::listen(sListen,2)==SOCKET_ERROR)
printf("Failedlisten()n");
return0;
//循环接受顾客的联接恳求
sockaddr_inremoteAddr;
intnAddrLen=sizeof(remoteAddr);
SOCKETsClient=0;
charszText[]="TCPServerDemo!rn";
while(sClient==0)
//接受一个新联接
//((SOCKADDR*)&remoteAddr)一个指向sockaddr_in结构的表针,用于获取对方地址
sClient=::accept(sListen,(SOCKADDR*)&remoteAddr,&nAddrLen);
if(sClient==INVALID_SOCKET)
printf("Failedaccept()");
printf("接受到一个联接:%srn",inet_ntoa(remoteAddr.sin_addr));
continue;
while(TRUE)
//向顾客端发送数据
gets(szText);
::send(sClient,szText,strlen(szText),0);
//从顾客端接收数据
charbuff[256];
intnRecv=::recv(sClient,buff,256,0);
if(nRecv>0)
buff[nRecv]='';
printf("接收到数据:%sn",buff);
//关掉同顾客端的联接
::closesocket(sClient);
//关掉窃听套节字
::closesocket(sListen);
return0;
顾客端代码:
#include"InitSock.h"
#include
#include
usingnamespacestd;
CInitSockinitSock;//初始化Winsock库
intmain()
//创建套节字
SOCKETs=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(s==INVALID_SOCKET)
printf("Failedsocket()n");
return0;
//也可以在这儿调用bind函数绑定一个本地地址
//否则系统将会手动安排
//填写远程地址信息
sockaddr_inservAddr;
servAddr.sin_family=AF_INET;
servAddr.sin_port=htons(4567);
//注意,这儿要填写服务器程序(TCPServer程序)所在机器的IP地址
//假如你的计算机没有联网,直接使用127.0.0.1即可
servAddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
if(::connect(s,(sockaddr*)&servAddr,sizeof(servAddr))==-1)
printf("Failedconnect()n");
return0;
charbuff[256];
charszText[256];
while(TRUE)
//从服务器端接收数据
intnRecv=::recv(s,buff,256,0);
if(nRecv>0)
buff[nRecv]='';
printf("接收到数据:%sn",buff);
//向服务器端发送数据
gets(szText);
szText[255]='';
::send(s,szText,strlen(szText),0);
//关掉套节字
::closesocket(s);
return0;
封装的InitSock.h
#include
#include
#include
#include
#pragmacomment(lib,"WS2_32")//链接到WS2_32.lib
classCInitSock
public:
CInitSock(BYTEminorVer=2,BYTEmajorVer=2)
//初始化WS2_32.dll
WSADATAwsaData;
WORDsockVersion=MAKEWORD(minorVer,majorVer);
if(::WSAStartup(sockVersion,&wsaData)!=0)
exit(0);
~CInitSock()
::WSACleanup();
};
须要C/C++Linux服务器开发学习资料加qun1023370945(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,解释器,DPDK,ffmpeg等)红旗linux安装,免费分享,希望你们一起努力提高技术,升职加薪走上人生颠峰!