socket编程原理

我们深知信息交换的价值,那么网络中的进程是如何相互通信的,比如我们每天打开浏览器浏览网页时,浏览器进程是如何与web服务器通信的?用QQ聊天时,QQ进程如何与服务器或好友所在的QQ进程进行通信?这一切都靠插座?那插座是什么?插座有哪些类型?还有socket的基本功能,这也是本文要介绍的。本文的主要内容如下:

1、网络中进程之间如何通信?2、Socket是什么?3、socket的基本操作3.1、socket()函数3.2、bind()函数3.3、listen()、connect()函数3.4、accept()函数3.5、read()、write()函数等3.6、close()函数4、socket中TCP的三次握手建立连接详解5、socket中TCP的四次握手释放连接详解6、一个例子1、网络中进程之间如何通信?

本地进程间通信(IPC)有多种方式,但可以归纳为以下四类:

消息传递(管道、FIFO、消息队列)同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)共享内存(匿名的和具名的)远程过程调用(Solaris门和Sun RPC)

但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信。首先要解决的问题是如何唯一标识一个进程,否则无法通信!进程可以由进程PID在本地唯一标识,但这在网络中是不可行的。事实上,TCP/ip协议家族已经帮助我们解决了这个问题。网络层的“IP地址”可以唯一标识网络中的主机,传输层的“协议+端口”可以唯一标识主机中的应用(进程)。这样就可以用三元组(ip地址、协议、端口)来标识网络中的进程,网络中的进程通信可以用这个标志与其他进程进行交互。

使用TCP/IP协议的应用程序通常使用应用程序编程接口:UNIX BSD的socket和UNIX System V(已过时)的TLI来实现网络进程间的通信。目前几乎所有的应用都使用socket,现在是网络时代,进程通信在网络中无处不在,这也是我说“一切都是socket”的原因。

2、什么是Socket?

我们已经知道网络中的进程是通过socket进行通信的,那么socket是什么呢?Socket起源于Unix,Unix/Linux的基本哲学之一是“一切都是文件”,可以作为“open-& gt;读写/读–>关闭”模式进行操作。我的理解是socket是这种模式的一种实现,socket是一种特殊的文件,有些Socket的功能就是在它上面的操作(读/写IO,打开和关闭)。我们将在后面介绍这些函数。

插座一词的由来

第一次在网络领域使用是在1970年2月12日发布的IETF RFC33中,作者是斯蒂芬·卡尔、史蒂夫·克罗克和温顿·瑟夫。根据美国计算机历史博物馆的记录,克罗克写道:“命名空之间的元素可以称为套接字接口。一个套接字接口构成一个连接的一端,一个连接可以完全由一对套接字接口指定。”计算机历史博物馆补充道:“这比BSD的socket接口定义早了12年左右。”

3、socket的基本操作

由于socket是“开-写/读-关”模式的实现,所以socket为这些操作提供了相应的函数接口。以TCP为例介绍几种基本的socket接口函数。

3.1、socket()函数

int socket(int域,int类型,int协议);socket函数对应的是普通文件的打开操作。一个普通的文件打开操作返回一个文件描述符,socket()用来创建一个套接字描述符,唯一标识一个套接字。这个套接字描述符与文件描述符相同,并且在后续操作中使用。把它作为一个参数,用它来执行一些读写操作。

就像您可以向fopen传递不同的参数值来打开不同的文件一样。创建套接字时,还可以指定不同的参数来创建不同的套接字描述符。套接字函数的三个参数是:

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。

注意:并不是以上所有类型和协议都可以随意组合。例如,SOCK_STREAM不能与IPPROTO_UDP结合使用。当协议为0时,将自动选择该类型对应的默认协议。

当我们调用socket创建套接字时,返回的套接字描述符存在于协议族(address family,AF_XXX)空中,但没有具体的地址。如果要给它分配一个地址,必须调用bind()函数,否则当你调用connect()和listen()时,系统会自动随机分配一个端口。

3.2、bind()函数

如上所述,bind()函数将地址族中的特定地址分配给套接字。比如对应AF_INET和AF_INET6就是给socket分配一个ipv4或者ipv6的地址和端口号组合。

int bind(int sockfd,const struct sockaddr *addr,socklen _ t addrlen);该函数的三个参数是:

sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
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 */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };ipv6对应的是:
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网络字节顺序和主机字节顺序; /* IPv6 address */ };Unix域对应的是:
#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };addrlen:对应的是地址的长度。

通常服务器启动时会绑定一个众所周知的地址(比如ip地址+端口号)来提供服务,让客户通过它连接服务器;客户端不需要指定,系统会自动分配一个端口号和自己的ip地址的组合。这也是为什么服务器在listen之前一般会调用bind(),而客户端不会调用,但是在connect()的时候系统会随机生成一个。

[16]

主机端序就是我们通常所说的大端和小端模式:不同的CPU有不同的端序类型,指的是整数在内存中存储的顺序。这称为主机端序。引用标准的大端和小端的定义如下:

A) Little-Endian表示低位字节在存储器的低位地址端释放,高位字节在存储器的高位地址端释放。

B) Big-Endian表示高位字节在存储器的低位地址端放电,低位字节在存储器的高位地址端放电。

网络字节顺序:4个字节的32位值按以下顺序传输:先0 ~ 7位,再8 ~ 15位,再16 ~ 23位,最后24 ~ 31位。这种传输顺序称为大端顺序。因为TCP/IP头中的所有二进制整数都要求在网络中按此顺序传输,所以也叫网络字节序。字节顺序,顾名思义,就是一个字节类型的数据在内存中存储的顺序,一个字节的数据是没有顺序的。

因此,在将地址绑定到套接字时,请先将主机字节顺序转换为网络字节顺序,不要像网络字节顺序一样假设主机字节顺序使用Big-Endian。因为这个问题,出现了血案!因为这个问题在公司的项目代码中,有很多令人费解的问题,所以请记住不要对主机字节顺序做任何假设,一定要转换成网络字节顺序后再赋给socket。

3.3、listen()、connect()函数

作为服务器,调用socket()和bind()后,会调用listen()来监听这个socket。如果客户端调用connect()发出连接请求,服务器将接收到这个请求。

int listen(int sockfd,int backlog);int connect(int sockfd,const struct sockaddr *addr,socklen _ t addrlen);listen函数的第一个参数是要监控的套接字描述符,第二个参数是相应套接字可以排队的最大连接数。socket()函数创建的socket默认为主动类型,listen函数将socket改为被动类型,等待客户的连接请求。

connect函数的第一个参数是客户端的套接字描述符,第二个参数是服务器的套接字地址,第三个参数是套接字地址的长度。客户端通过调用connect函数与TCP服务器建立连接。

3.4、accept()函数

TCP服务器在依次调用socket()、bind()和listen()后,会监听指定的套接字地址。TCP客户端依次调用socket()和connect()后,向TCP服务器发送连接请求。TCP服务器监听到这个请求后,会调用accept()函数来接收请求,从而建立连接。之后就可以开始网络I/O操作了,类似于普通文件的读写I/O操作。

int accept(int sockfd,struct sockaddr *addr,socklen _ t * addrlen);accept函数的第一个参数是服务器的套接字描述符,第二个参数是指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数是协议地址的长度。如果accpet成功,其返回值是内核自动生成的全新描述符,代表与返回客户的TCP连接。

注意:accept的第一个参数是服务器的套接字描述符,在服务器开始调用socket()函数时生成,称为监控套接字描述符;accept函数返回连接的套接字描述符。一个服务器通常只创建一个监听套接字描述符,它总是存在于服务器的生命周期中。内核为服务器进程接受的每个客户机连接创建一个连接套接字描述符。当服务器完成服务客户端时,相应的连接套接字描述符被关闭。

3.5、read()、write()等函数

一切都只是东风,到目前为止服务器已经和客户建立了良好的联系。可以调用网络I/O读写,也就是实现了网络中不同进程之间的通信!网络I/O操作分为以下几组:

read()/write()recv()/send()readv()/writev()recvmsg()/sendmsg()recvfrom()/sendto()

我推荐recvmsg()/sendmsg()函数。这两个函数是最常见的I/O函数。事实上,你可以用这两个函数代替上面所有的其他函数。他们的声明如下:

# include & ltunistd.h & gtssize_t read(int fd,void *buf,size _ t count);ssize_t write(int fd,const void *buf,size _ t count);# include & ltsys/types . h & gt;# include & ltsys/socket . h & gt;ssize_t send(int sockfd,const void *buf,size_t len,int flags);ssize_t recv(int sockfd,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 recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen _ t * addrlen);ssize_t sendmsg(int sockfd,const struct msghdr *msg,int flags);ssize_t recvmsg(int sockfd,struct msghdr *msg,int flags);read函数负责从fd中读取内容。当读取成功时,read返回实际读取的字节数。如果返回值为0,则表示已经读取了文件的结尾,小于0则表示发生了错误。如果错误是EINTR,说明读取是中断造成的,如果是ECONNREST,说明网络连接有问题。

write函数将buf中nbytes字节的内容写入文件描述符fd。如果成功,则返回写入的字节数。失败时返回-1并设置errno变量。在网络程序中,当我们写入socket文件描述符时,有两种可能。1)1)write的返回值大于0,表示部分或全部数据已被写入。2)返回值小于0,出现错误。我们应该根据错误的类型来处理它。如果错误为EINTR,则意味着写入时发生了中断错误。如果EPIPE表示网络连接有问题(对方已经关闭连接)。

这几对I/O函数我就不一一介绍了。详情参见man文档或百度和Google。send/recv将在下面的示例中使用。

3.6、close()函数

服务器和客户端建立连接后,会进行一些读写操作。当读写操作完成后,相应的socket描述符会被关闭,就像操作结束后调用fclose关闭打开的文件一样。

# include & ltunistd.h & gtint close(int FD);当关闭TCP套接字的默认行为时,将套接字标记为关闭,然后立即返回到调用进程。该描述符不能再被调用进程使用,也就是说,它不能再被用作read或write的第一个参数。

注意:关闭操作只使对应的套接字描述符的引用计数为-1,只有当引用计数为0时,才会触发TCP客户端向服务器发送连接终止请求。

4、socket中TCP的三次握手建立连接详解

我们知道tcp是通过“三次握手”建立连接的,也就是交换三个包。一般流程如下:

客户端向服务器发送一个SYN J服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1客户端再想服务器发一个确认ACK K+1

只会完成三次握手,但是socket的函数里这个三次握手呢?请看下图:

socket编程原理

图一。在套接字中发送的TCP三次握手

从图中可以看出,当客户端调用connect时,会触发一个连接请求,向服务器端发送一个SYN J包。此时connect进入阻塞状态;服务器监听连接请求,即接收到SYN J包,调用accept函数接收请求并向客户端发送SYN K和ACK J+1,此时accept进入阻塞状态;客户端收到服务器的SYN K和ACK J+1后,connect返回并确认SYN K;当服务器收到ACK+1时,accept返回,三次握手结束,连接建立。

总结:在三次握手中第二次返回的客户端连接,在三次握手中第三次返回的服务器端接受。

5、socket中TCP的四次握手释放连接详解

以上描述了在socket中建立TCP三次握手的过程,以及涉及到的socket函数。现在介绍一下socket中四次握手释放连接的过程。请看下图:

图二。在套接字中发送的TCP四次握手

举例说明的过程如下:

某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;接收到这个FIN的源发送端TCP对它进行确认。

所以每个方向都有一个FIN和一个ACK。

6.下面给出了一个实现的例子。

首先给出实现的截图。

服务器端代码如下:

#包括& # 34;InitSock.h & # 34

# include & ltstdio.h & gt

# include & ltiostream & gt

使用命名空间std

CInitSock initSock//初始化Winsock库

int main()

{

//创建套接字

SOCKET sListen = ::socket(AF_INET,SOCK_STREAM,IP proto _ TCP);

//用于指定套接字使用的地址格式,通常使用AF_INET。

//指定套接字的类型。如果使用SOCK_DGRAM,则使用udp不可靠传输。

//与type参数一起使用,指定使用的协议类型(指定套接字类型时,可以设置为0,因为默认为UDP或TCP)。

if(sListen == INVALID_SOCKET)

{

printf(& # 34;套接字()失败\ n & # 34);

返回0;

}

//填充sockaddr_in结构,这是一个结构。

/* struct sockaddr_in {

短sin _ family//地址族(指定地址格式),设置为AF_INET。

u _ short sin _ port//端口号

结构in _ addr sin _ addr//IP地址

char sin _ zero[8];/空子部分,设置为空

} */

sockaddr _ in sin

sin.sin _ family = AF _ INET

sin . sin _ port = htons(4567);//1024 ~ 49151:普通用户注册的端口号。

sin.sin_addrS_un。S _ addr = INADDR _ ANY

//将这个套接字绑定到本地地址

if(::bind(sListen,(LPSOCKADDR)& amp;sin,sizeof(sin)) ==套接字_错误)

{

printf(& # 34;绑定失败()\ n & # 34);

返回0;

}

//进入监听模式

//2表示允许保留在侦听队列中的未处理连接的最大数量。

if(::listen(sListen,2) == SOCKET_ERROR)

{

printf(& # 34;listen()失败\ n & # 34);

返回0;

}

//循环接受客户的连接请求。

sockaddr _ in remoteAddr

int nAddrLen = sizeof(remote addr);

套接字s client = 0;

char SZ text[]= & # 34;TCP服务器演示!\ r \ n & # 34;

while(sClient==0)

{

//接受新的连接

//((SOCKADDR *)& amp;remote addr)sockaddr _ in结构的指针,用来获取对方的地址。

sClient = ::accept(sListen,(SOCKADDR *)& amp;remote addr & amp;nAddrLen);

if(sClient == INVALID_SOCKET)

{

printf(& # 34;接受失败()& # 34;);

}

printf(& # 34;接收到一个连接:% s \ r \ n & # 34,inet _ ntoa(remote addr . sin _ addr));

继续;

}

while(真)

{

//向客户端发送数据

gets(SZ text);

* send(s client,szText,strlen(szText),0);

//从客户端接收数据

字符缓冲器[256];

int nRecv = ::recv(sClient,buff,256,0);

if(nRecv & gt;0)

{

buff[nRecv]= & # 39;[120]';

printf(& # 34;接收的数据:% s \ n & # 34,buff);

}

}

//关闭与客户端的连接。

* close socket(s client);

//关闭监视器套接字

* close socket(s listen);

返回0;

}

客户代码:

#包括& # 34;InitSock.h & # 34

# include & ltstdio.h & gt

# include & ltiostream & gt

使用命名空间std

CInitSock initSock//初始化Winsock库

int main()

{

//创建套接字

SOCKET s = ::socket(AF_INET,SOCK_STREAM,IP proto _ TCP);

if(s == INVALID_SOCKET)

{

printf(& # 34;套接字()失败\ n & # 34);

返回0;

}

//也可以在这里调用bind函数绑定一个本地地址。

//否则系统会自动安排。

//填写远程地址信息

sockaddr _ in servAddr

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(& # 34;127.0.0.1");

if(::connect(s,(sockaddr *)& amp;servAddr,sizeof(servAddr)) == -1)

{

printf(& # 34;连接失败()\ n & # 34);

返回0;

}

字符缓冲器[256];

char SZ text[256];

while(真)

{

//从服务器接收数据。

int nRecv = ::recv(s,buff,256,0);

if(nRecv & gt;0)

{

buff[nRecv]= & # 39;[167]';

printf(& # 34;接收的数据:% s \ n & # 34,buff);

}

//向服务器发送数据。

gets(SZ text);

SZ text[255]= & # 39;[172]';

* send(s,szText,strlen(szText),0);

}

//关闭套接字

*封闭式插座;

返回0;

}

封装的InitSock.h

# include & ltwinsock2.h & gt

# include & ltstdlib.h & gt

# include & ltconio.h & gt

# include & ltstdio.h & gt

#pragma注释(lib,& # 34;WS2 _ 32 & # 34)//链接到WS2_32.lib

辛尼索克级

{

公共:

CInitSock(字节minorVer = 2,字节majorVer = 2)

{

//初始化WS2_32.dll

WSADATA wsaData

WORD sock version = make WORD(min orver,major ver);

if(::WSAStartup(sockVersion,& ampwsaData)!= 0)

{

退出(0);

}

}

~辛尼索克()

{

* WSACleanup();

}

};

需要C/C++ Linux服务器开发学习资料“资料”(资料包括C/C++、Linux、golang technology、Nginx、ZeroMQ、MySQL、Redis、fastdfs、MongoDB、ZK、流媒体、CDN、P2P、K8S、Docker、TCP/IP、协诚、DPDK、ffmpeg等

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。

发表回复

登录后才能评论