网络编程(三)—— 套接字的使用&建立网络连接

文章目录

    • 1 服务器准备连接
      • 1.1 socket():预准备手机
      • 1.2 bind():绑定电话卡
      • 1.3 listen():真正的可用手机
      • 1.4 accept():电话铃声响起
    • 2 客户端发请求连接
      • 2.1 connect():拨打服务器的电话
      • 2.2 浅读一下TCP三次握手

在这里插入图片描述

前面学习了IPv4、IPv6、本地套接字的相关知识,现在学习一下怎么去使用前面所学的知识——使用对应套接字格式完成网络连接的建立
先有服务而后有客户

1 服务器准备连接

1.1 socket():预准备手机

在这里插入图片描述

要创建一个套接字需要用到下面的函数

int socket(int domain, int type, int protocol)
  • domian:PF_INET、PF_INET6、PF_LOCAL(AF_XXX)等
  • type:
    • SOCK_STREAM:表示的是字节流,对应TCP
    • SOCK_DGRAM:表示的是数据报,对应UDP
    • SOCK_RAW:表示原始套接字(少用到)
  • protocol:原用于指定通信协议,因前两个参数已基本固定一个协议,所以这里基本上设置为0即可

1.2 bind():绑定电话卡

在这里插入图片描述

由前面socket()函数准备好的套接字如果要被使用,那么需要给它提供一个信号源,这就需要用到bind()函数把套接字和套接字地址绑定,也就是将手机卡装入手机的SIM卡槽

bind(int fd, sockaddr * addr, socklen_t len)
  • fd:表示的是我们前面所创建出来的初始socket句柄
  • addr:通用地址格式,用以适配IPv4、IPv6、本地套接字格式等
  • len:通用地址的长度,通过这个参数确定传入的地址格式是哪个类型的

如果是双卡双待手机,那该怎么绑定手机卡呢?
这个时候,可以利用通配地址的能力帮助我们解决这个问题
对IPv4地址来说,通配地址的设置通过INADDR_ANY来完成;对于IPv6地址来说,则是通过INADDR6_ANY来完成的

struct sockaddr_in name;
name.sin_addr.s_addr = htonl (INADDR_ANY); /* IPV4通配地址 */

除了设置地址外,还有一个参数就是端口号。如果把端口号设置为0,那么系统内核会按照一定的算法来选择一个空闲的端口,以完成socket的绑定(这在服务器端中并不常用,因为一般来说,服务器端需要绑定一个已知的地址和端口号,否则客户就不知道怎么联系上服务器)

下面是初始化IPv4 TCP socket的例子

#include 
#include 
#include 
#include 


int make_socket (uint16_t port)
{
  int sock;
  struct sockaddr_in name;


  /* 创建字节流类型的IPV4 socket. */
  sock = socket (PF_INET, SOCK_STREAM, 0);
  if (sock < 0)
    {
      perror ("socket");
      exit (EXIT_FAILURE);
    }


  /* 绑定到port和ip. */
  name.sin_family = AF_INET; /* IPV4 */
  name.sin_port = htons (port);  /* 指定端口 */
  name.sin_addr.s_addr = htonl (INADDR_ANY); /* 通配地址 */
  /* 把IPV4地址转换成通用地址格式,同时传递长度 */
  if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0)
    {
      perror ("bind");
      exit (EXIT_FAILURE);
    }
  return sock
}

1.3 listen():真正的可用手机

在这里插入图片描述

经过绑定电话卡后,一个正常可用的手机就诞生了,此时的手机都是自己准备好,以便可以使用的,这一系列过程都是主动性的,但是作为服务器端,需要“高冷”一点,不能太过主动,应该等待别人的主动。
listen()将原本的主动性socket转化为被动性的socket,以告诉系统内核:“我现在只会等待别人的主动通知,我是用来被请求连接的!”

int listen (int socketfd, int backlog)
  • socketfd:套接字描述符,即上面绑定好手机卡后的socket
  • backlog:在Linux中表示已完成(ESTABLISHED)且未accept的队列大小,这个参数的大小决定了可以接收的并发数目。参数越大理论上并发数目越大,但是数目过大会占用系统资源

到这里,服务器端已经准备的差不多了,即将进入最后的等待模式

什么是并发?
在这里插入图片描述
百度了一下的知识,好像理解了但是又有点疑惑。

  • 并发:并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)
  • 并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

上面的两个概念摘自并发,这里并行很好理解,但是并发里说的多个线程运行在一个时间段里同时运行怎么理解呢?这个先记下来后面看明白了再回来补充

1.4 accept():电话铃声响起

在这里插入图片描述

当客户端的连接请求到达时,服务器端应答成功,连接建立,这个时候操作系统内核需要把这个事件通知到应用程序,并让应用程序感知到这个连接。这个过程,就好比电信运营商完成了一次电话连接的建立, 应答方的电话铃声响起,通知有人拨打了号码,这个时候就需要拿起电话筒开始应答。

int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)
  • listensockfd:从名字上可以看出就是经过listen()转化后的被动socket
  • cliaddr:通过指针获取客户端的地址
  • addrlen:告诉我们地址的大小
    这个函数的过程可以理解成当我们拿起电话机时,看到了来电显示,知道了对方的号码
    需要注意的是,传入的是监听式套接字描述符,返回的是已连接的套接字描述符,两者不是同一个描述符

为什么不把输入的套接字描述符和返回的描述符并为同一个?
这里就用到了上面所说到的并发知识
和打电话的情形非常不一样的地方就在于,打电话一旦有一个连接建立,别人是不能再打进来的,只会得到语音播报:“您拨的电话正在通话中。”而网络程序的一个重要特征就是并发处理,不可能一个应用程序运行之后只能服务一个客户,如果是这样, 双 11 抢购得需要多少服务器才能满足全国 “剁手党 ” 的需求?所以监听套接字一直都存在,它是要为成千上万的客户来服务的,直到这个监听套接字关闭;而一旦一个客户和服务器连接成功,完成了 TCP 三次握手,操作系统内核就为这个客户生成一个已连接套接字,让应用服务器使用这个已连接套接字和客户进行通信处理。如果应用服务器完成了对这个客户的服务,比如一次网购下单,一次付款成功,那么关闭的就是已连接套接字,这样就完成了 TCP 连接的释放。请注意,这个时候释放的只是这一个客户连接,其它被服务的客户连接可能还存在。最重要的是,监听套接字一直都处于“监听”状态,等待新的客户请求到达并服务

2 客户端发请求连接

第一步还是和服务端一样,要建立一个套接字,方法和前面是一样的。不一样的是客户端需要调用 connect 向服务端发起请求。

2.1 connect():拨打服务器的电话

在这里插入图片描述

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
  • sockfd:socket()创建的套接字描述符
  • servaddr:指向套接字地址结构的指针
  • addrlen:套接字地址结构的长度大小

connect()之前不需要bind()吗?
客户端在调用connect()之前不一定需要调用bind()函数,客户端作为主动方,发起的请求连接目标可以是多个,所以并不需要使用bind()进行目标绑定。如果需要使用bind()的话,内核会确定源IP地址,按照一定算法分配一个临时端口。

回想上一章节中的知识,(TCP socket)当程序执行到客户端的connect()时,就已经触发了TCP中的三次握手机制了,而且仅在连接建立成功或者出错后才返回。连接建立成功返回很好理解,那什么是连接出错呢?

  1. 三次握手无法建立,客户端发出的 SYN 包没有任何响应,于是返回 TIMEOUT 错误。这种情况比较常见的原因是对应的服务端 IP 写错。
  2. 客户端收到了 RST(复位)回答,这时候客户端会立即返回 CONNECTION REFUSED 错误。这种情况比较常见于客户端发送连接请求时的请求端口写错,因为 RST 是 TCP 在发生错误时发送的一种 TCP 分节。产生 RST 的三个条件是:目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器(如前所述);TCP 想取消一个已有连接;TCP 接收到一个根本不存在的连接上的分节。
  3. 客户发出的 SYN 包在网络上引起了"destination unreachable",即目的不可达的错误。这种情况比较常见的原因是客户端和服务器端路由不通。

2.2 浅读一下TCP三次握手

网络编程(三)—— 套接字的使用&建立网络连接_第1张图片

在这里插入图片描述

网上搜索到的简单讲解:

  1. 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 j,客户端进入 SYNC_SENT 状态;
  2. 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,表示对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;
  3. 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1;
  4. 应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。

再以打电话为例子来浅读一下这几个知识点:(W:卧底,P:指挥官)

  1. W给P打电话:“摩西摩西,我上线了,我方暗号是j,收到请回答,over”
  2. P接通W的电话,听到了W的暗号内容,确认了W的身份
  3. 确认W身份后,回复到:“我收到了你的暗号j,我方已准备好,随时可以行动,代号为k,请接受指令”
  4. W收到P的回复后,表示:“收到命令k,over。Just do it”

你可能感兴趣的:(网络编程,c++,tcp/ip,websocket,网络协议)