socket基础之c/s通信过程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gc348342215/article/details/70495717

socket可以看成是用户进程与内核网络协议栈的编程接口,socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程通信。
这里所说的不同主机例如手机与PC的通信,为什么说手机与PC为不同主机?只是因为其外观不同吗?不是的,首先手机与PC的软件不同,其次是两者的硬件架构不同,手机采用的是ARM架构而PC大多是x86的架构(至于这两者架构有什么区别,博主现在还不了解,待后续补充,先自行查看参考资料)

下图为client(用户)与server(服务器)之间的通信过程。用到了socket(),connect(),bind(),listen(),accept(),write(),read()等函数。
C/S通信方式


socket基础之client与server之间的通信过程

首先说说什么是字节序,字节序分为大端字节序和小端字节序。

大端字节序:最高有效位存储于最低内存地址处,最低有效位存储于最高内存地址。
小端字节序:最高有效位存储于最高内存地址处,最低有效位存储于最低内存地址。

主机字节序:不同的主机有不同的字节序。
(1)x86硬件架构下的主机为小端字节序
(2)Motorola 68000为大端字节序
(3)ARM架构的主机字节序是可配置的

网络字节序:网络字节序规定为大端字节序。

异构系统传输规定将主机A的字节序转化为网络字节序,再将网络字节序转化为主机B的字节序。(重要)(这里所说的异构系统可以理解为不同架构的系统,上面说的主机字节序因为架构不同,所产生的字节序也不同,所以规定统一先转化为网络字节序)

字节序之间的转换要用到字节序转化函数:

#include <netinet/in.h>
uint16_t    htons(uint16_thost16bitvalue);
uint32_t    htonl(uint32_thost32bitvalue);

上面这两种均返回网络字节序的值

uint16_t    ntohs(uint16_tnet16bitvalue);
uint32_t    ntohl(uint32_tnet32bitvalue);

上面这两种均返回主机字节序的值

h代表host n代表network s代表short l代表long

这些函数后续博客都会详细说明


对字节序有一定了解后我们来说socket套接字地址结构,定义在#include <netinet/in.h >中。
IPv4套接字地址结构如下:

struct in_addr {
       in_addr_t   s_addr; 
};
//存储32位IPv4网络字节序地址(所以要将例如127.0.0.1这样的点分十进制地址转换,用到inet_aton函数)
struct sockaddr_in {
       sa_family_t        sin_family;
       in_port_t           sin_port; 
       struct in_addr     sin_addr;
       char                  sin_zero[8];
};
//sin_family写入AF_INET,表示IPv4的地址族
//sin_port存储端口号(端口号使用网络字节序,必须用到htons函数将16位TCP或UDP端口号转换成网络字节序),在linux下端口号范围是0-65535(端口在IP头部占16个字节),同时0-1023端口号已经被系统使用或保留
//sin_zero[8]字段值未曾使用,设置为0即可

client/server使用socket()函数创建套接字:

#include < sys/socket.h >
int socket ( int family, int type,int protocol );
//socket函数在成功时返回一个小的非负整数值,它与文件描述符类似,我们把它称为套接字描述符(socketdescriptor),简称sockfd。失败时则返回-1。

socket函数(非阻塞)参数解释:

  • int family参数指明协议族:
AF_INET                //IPv4协议
AF_INET6               //IPv6协议
  • int type参数指明套接字类型:
SOCK_STREAM            //字节流套接字(表示用TCP传输,可靠、双向、面向链接的比特流)
SOCK_DGRAM             //数据报套接字(表示用UDP传输,不可靠、无连接通信)
SOCK_RAW               //原始套接字
  • intprotocol参数指明协议类型(也可设置为0,当设置为0时选择family和type组合来确定传输协议):
IPPROTO_TCP            //TCP传输协议
IPPROTO_UDP            //UDP传输协议

socket的目的是为了向内核申请一个描述符,所以这也是socket不阻塞的原因。


server使用bind()函数绑定套接口:

#include <sys/socket.h>
int bind (int sockfd, const structsockaddr *myaddr, socklen_t addrlen); 
// 若成功则返回0,若出错则返回-1。

bind函数作用:将IP、端口命名给socket,让它具有远程通信的功能。

bind函数(非阻塞)参数解释:

  • int sockfd参数是socket套接字创建后返回的套接字描述符
  • const struct sockaddr*myaddr参数是取特定协议地址结构(例如IPv4的sockaddr_in)
  • socklen_t addrlen参数表示所指向的特定协议地址结构的结构体大小

一般情况下客户端没必要使用bind绑定socket,因为在发送数据报文的时候,内核会帮你填写好报文头部的源IP和源端口,但是需要在client中初始化服务器端的IP和端口,这样才能在connect之后将报文发送至目的IP和目的端口。bind函数一般用于服务器,服务器端的第二个结构体参数不用初始化,因为当服务器accept时,服务器端接受报文后会将报文头部的源IP和源端口写入服务器的structsockaddr_in结构体中。

什么时候bind成功呢?当绑定的ip、port均合法的情况下即会成功。bind如果绑定端口号为0,则由系统为主机分配一个本地可用端口来使用。


server使用listen()函数监听socket套接字:

#include < sys/socket.h >
int listen ( int sockfd, int backlog);
//若成功则返回0,若出错则返回-1。

listen函数(非阻塞)参数解释:

  • int sockfd参数指的是需要进入监听状态的socket套接字返回的套接字描述符
  • int backlog参数指的是请求队列的最大长度(kernel2.2之前这个参数代表了半连接与完全连接的个数,kernel2.6之后为完全连接的最小值,这是一个幻数,自己定义的请求队列的大小是一个值,但实际上可能允许的最大队列小于或大于自己定义的这个值,即min(backlog,/proc/sys/net/core/somaxconn),默认情况下,somaxconn的值为128(我的centos上是128),表示最多有129个ESTABLISHED的连接等待accept())

listen函数仅由TCP服务器调用,所谓监听是指当没有客户端(client)的connect函数请求时,套接字处于“睡眠”状态,只有当接收到client的connect请求时,socket套接字才被“唤醒”来响应请求。注意:listen函数只是让套接字处于监听状态,并没有接收请求。接收请求需要使用accept函数。

listen函数的执行在TCP三次握手之前,而accept函数的执行则在TCP三次握手之后。

int backlog参数设置请求队列的最大长度,那么什么是请求队列呢?

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区(从半连接队列到玩全连接队列),待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(半连接队列和玩全连接队列(详细戳这里))(RequestQueue)。

缓冲区的长度(能存放多少个client请求)可以通过 listen() 函数的backlog参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。如果将 backlog的值设为SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。当请求队列满时,就不再接收新的请求,对于Linux,client会收到 ECONNREFUSED 错误。


server使用accept函数来等待接收client的connect请求:

#include < sys/socket.h>
int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
//若成功则返回新的非负描述符connfd,若出错则为-1

accept函数参数解释:

  • intsockfd参数为监听套接字描述符(由socket创建,随后用作bind,listen的第一个参数)
  • struct sockaddr *cliaddr参数是取client的特定协议地址结构(一般由client填写)
  • socklen_t*addrlen参数指向一个局部整形变量,一般设置为(sizeof(struct sockaddr_in))

accept函数由TCP服务器调用,用于从完全连接队列队头返回下一个已完成连接。如果完全连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)。
需要注意的一点是如果我们对返回客户协议地址不感兴趣,那么可以把cliaddr和addrlen均置为空指针,第二个参数为NULL,第三个也填写为NULL即可

一个服务器通常仅仅创建一个监听套接字(sockfd),它在该服务器生命周期内一直存在。内核为每个由服务器进程接受的client连接创建一个已连接套接字(accept返回的描述符)。当server完成对某个给定的client的服务时,相应的已连接套接字就被关闭。accept返回的描述符标记了协议,源IP PORT,目的IP PORT,所以可以唯一确认一个连接。


参考资料:
http://c.biancheng.net/cpp/html/3029.html(socket简介)
http://blog.csdn.net/hguisu/article/details/7445768(socket详解)
http://ihyperwin.iteye.com/blog/1701132(ARM与x86的区别)
http://c.biancheng.net/cpp/html/3032.html(使用socket创建套接字)
https://my.oschina.net/winHerson/blog/181894(socket地址结构详解)
https://msdn.microsoft.com/en-us/library/windows/hardware/ff543744(v=vs.85).aspx(AF_INET的解释)
http://c.biancheng.net/cpp/html/3036.html(listen和accept函数详解)
http://blog.csdn.net/daiyudong2020/article/details/51868813(accept函数详解)
http://blog.chinaunix.net/uid-97185-id-4332435.html(read.write.send.recv函数比较)

猜你喜欢

转载自blog.csdn.net/gc348342215/article/details/70495717