从哪看出网站的建站公司,wordpress显示文章列表的主题,百度域名查询官网,中山做app网站公司哪家好前前言#xff1a;大家看到这一章节的时候一定不要跳过#xff0c;虽然标题是编程#xff0c;但实际上是对 socket 的运行机制做了详细的讨论#xff0c;对理解 TCP 有很大的帮助#xff1b;但是由于本节涉及到了大量的编程知识#xff0c;对于一些朋友来说不是很好理解大家看到这一章节的时候一定不要跳过虽然标题是编程但实际上是对 socket 的运行机制做了详细的讨论对理解 TCP 有很大的帮助但是由于本节涉及到了大量的编程知识对于一些朋友来说不是很好理解所以大家看本节的时候强烈建议结合我的这篇笔记来学习本篇整理了许多详细的案例和解析也补充了我自己的理解导致这篇笔记来到了七千多字工程量很大如果这篇文章对你有所帮助别忘了留下你的点赞和关注 前言 学习视频中科大郑烇、杨坚全套《计算机网络自顶向下方法 第7版James F.KuroseKeith W.Ross》课程 该视频是B站非常著名的计网学习视频但相信很多朋友和我一样在听完前面的部分发现信息量过大有太多无法理解的地方在我第一次点开的时候也有相同的感受但经过了一段时间项目的学习对计网有了更多的了解所以我准备在这次学习的时候做一些记录并且加入一些我的理解希望能够帮助到大家。 往期笔记可以看专栏中的内容 文章目录 2.8 TCP 套接字编程2.8.1 通过 TCP 建立连接的流程2.8.2 编程案例2.8.3 总结 2.8 TCP 套接字编程 Socket 编程应用进程使用 传输层 提供的服务才能够交换报文实现应用协议从而实现引用。 在 TCP/ IP 的架构中应用进程使用 Socket API 来 访问 传输的服务传输层中 TCP 提供的是可靠的 字节流 的服务UDP 提供的是不可靠的数据 UDP 数据报的服务字节流需要上层自己来维护界限也就是 TCP 提供字节流但是需要自己来划清有哪些部分 2.8.1 通过 TCP 建立连接的流程 前期准备 服务器 服务器进程必须先处于运行的状态 创建欢迎 socket 和本地的 端口 捆绑 在原本创建的 socket 上阻塞式等待接收用户的连接 客户端 创建客户端本地的 socket捆绑到本地的端口 与指定的服务器的进程和 IP 地址和端口发送建立连接的请求然后这个 socket 会在本地等候连接 服务器 当服务器接收到用户的请求时接触阻塞创建一个新的 socket 值与客户端通信 原初的监听 socket 并不会消失它继续存在并可以接收更多的客户端连接请求。服务器端的监听 socket 负责监听和接受连接。 客户端 客户端收到信息后确认收到信息再次向客户端发送报文表示确认链接 上面的过程其实就是简化过的 TCP 三次握手 的过程 上述过程中的端口中服务器的端口是指定的就是对外提供服务的端口每个服务器都有对外提供访问服务的端口而客户端的端口可能是随机指定的比如编写一段 Java 程序跑在 8080 端口程序向数据库服务器发送请求作为客户端数据库服务器提供的访问端口为 3306返回的数据不会直接发送到Java程序监听的8080端口而是发回到客户端在连接时使用的随机源端口上。 这个例子是为了让大家更好的理解服务器的客户端因为在命令执行的时候服务器其实就是发送请求的客户端而 同时 又作为对外提供服务的服务器。 2.8.2 编程案例 这里使用一个案例来更好的描述 socket 的运转流程 客户端接收一个仅包含小写字母的输入将输入部分发送给服务器服务器将其转换为大写并且返回 在开始之前先来介绍两个结构体存储信息的一种数据结构 sockaddr_in是 C 语言编程中用于表示 Internet (IPv4) 地址的结构体通常在 BSD sockets API 中使用。
struct sockaddr_in {sa_family_t sin_family; // 地址族对于 IPv4 应该为 AF_INET通常定义为2in_port_t sin_port; // 端口号网络字节序即大端字节序struct in_addr sin_addr; // IPv4 地址结构体unsigned char sin_zero[8]; // 填充字段
};概念辨析 地址族地址族用来定义不同类型的网络层协议以及这些协议如何识别并处理网络地址。每一种地址族对应一种特定的网络协议或一系列相关的协议支持相应的地址格式和相关功能 比如 AF_INET 代表 IPv4 地址族 填充字段对于不同的网络协议IPv4、IPv6 等是将其对应的结构转换为一个通用的结构来进行信息的传递的所以能够将其转换为这个统一的结构且不会出现匹配问题就需要一个填充字段来使得更具备兼容性。 再来看这个结构体的存储信息的意义
这个结构体在通信过程中表示的是通信的 一方当客户端或服务器需要指定一个网络地址进行连接、绑定或接收连接时就会使用 sockaddr_in 结构体这个端点既可以是本地端例如服务器绑定到的IP地址和端口也可以是远程端例如客户端要连接的目标服务器的IP地址和端口。 hostent在C语言的网络编程中用于存储与主机名解析相关的信息它包含了从域名系统DNS获取到的一个特定主机的所有相关信息。
struct hostent {char *h_name; // 主机的正式名字规范化主机名char **h_aliases; // 指向别名列表的指针数组int h_addrtype; // 地址类型例如AF_INETIPv4、AF_INET6IPv6int h_length; // 地址长度以字节为单位char **h_addr_list; // 指向主机IP地址列表的指针数组
};这一部分是结合完整的 C 语言代码的演示不用去看懂代码熟悉完整的流程即可 首先服务器启动程序并且创建一个 欢迎 socket 来等待客户端的请求
第一步变量声明 int server_sock, client_sock;struct sockaddr_in server_addr, client_addr;socklen_t client_len sizeof(client_addr);char buffer[BUFFER_SIZE];这里先不对每个变量的意义做出解释后面在流程中一一阐述 第二步创建 socket // 创建 socket if ((server_sock socket(AF_INET, SOCK_STREAM, 0)) -1) {perror(socket creation failed);exit(EXIT_FAILURE);}解析 server_sock在之前的 socket 部分提到过上层应用访问 socket 是通过一个整型这个整型就标识着这个 socket。而调用函数创建一个 socket 返回的就是这个标识。上述的创建过程包括很多后续的创建过程都是通过 if 创建的旨在如果创建失败的话会执行 if 中的语句来做失败处理是一种常见的方式。 第三步设置服务器地址结构体也就是上面提到的 sockaddr_in // 设置服务器地址结构体memset(server_addr, 0, sizeof(server_addr));server_addr.sin_family AF_INET;server_addr.sin_port htons(8000); // 假设监听8000端口server_addr.sin_addr.s_addr htonl(INADDR_ANY); // 绑定到所有网络接口AF_INET 就是上面提到的 IPv4 协议族 第四步绑定 socket 到指定的端口 // 绑定 socket 到指定端口if (bind(server_sock, (struct sockaddr *)server_addr, sizeof(server_addr)) -1) {perror(bind failed);exit(EXIT_FAILURE);}这一步就是将创建的 socket 实实在在的绑定到本机的端口上了 在本步骤之前socket 都是一个系统返回的整型在本步骤之后它就与本地的端口做了绑定上面的 sockaddr 就是上面提到的统一的数据结构绑定等操作不会直接对 server_addr 或者 client_addr 做操作而是将其转换为 sockaddr 结构。 第五步开始监听请求 // 开始监听连接请求if (listen(server_sock, 10) -1) { // 最多允许10个未处理的连接请求排队perror(listen failed);exit(EXIT_FAILURE);}创建完上面的 socket也就是常说的 HelloSocket 并且将请求放于队列中等待这个队列最多存储十个请求当请求多于十个的时候会拒绝请求。通过这个命令将 socket 置于监听的状态 接下来将视角转向客户端
第一步变量声明 int client_sock;struct sockaddr_in server_addr;char buffer[BUFFER_SIZE];char input[BUFFER_SIZE];第二步创建 socket // 创建 socketif ((client_sock socket(AF_INET, SOCK_STREAM, 0)) -1) {perror(socket creation failed);exit(EXIT_FAILURE);}这个 socket 是标识本地的 socket 与服务器不同的是服务器需要额外的去指定自己的端口不能随便分配因为这个端口是要告知客户端来使其访问的所以对于客户端的 socket 来说不需要去手动指定端口而是在调用 connect()函数与服务器建立连接时操作系统会 自动 为该socket分配一个可用的临时端口作为源端口这个端口号通常是随机选择的范围是1024至65535。 第三步设置服务器地址的结构体 // 设置服务器地址结构体memset(server_addr, 0, sizeof(server_addr));server_addr.sin_family AF_INET;server_addr.sin_port htons(SERVER_PORT);if (inet_pton(AF_INET, SERVER_IP, server_addr.sin_addr) 0) {perror(invalid address/ Address not supported);exit(EXIT_FAILURE);}服务器的信息是提前告知用户的 中间可能会通过 hostent 来通过域名解析得到 IP 地址本例子中没有涉及 第四步与服务器建立连接
// 连接到服务器
if (connect(client_sock, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) {perror(connect failed);exit(EXIT_FAILURE);
}需要传入本地的 socket 和服务器的信息 继续回到服务器
服务器本地等待连接 while (1) {
// 接受客户端连接请求if ((client_sock accept(server_sock, (struct sockaddr *)client_addr ,client_len)) 0) {perror(accept failed);continue;}// 其他代码
}这段代码之后服务器的代码都是写在这个 while 函数中的 accept 函数会阻塞的等待连接当 listen 队列中有连接的时候会去除并且处理 回到客户端来发送信息
发送数据到服务器 // 发送数据到服务器if (send(client_sock, input, strlen(input), 0) 0) {perror(send failed);exit(EXIT_FAILURE);}当建立连接后客户端就可以发送信息了 这时候本地的 socket 就与服务器连接上了通过这个 socket 就可以实现与服务器的通信了 服务器来处理客户端的请求 // 接收客户端发送的数据ssize_t bytes_received recv(client_sock, buffer, BUFFER_SIZE - 1, 0);if (bytes_received 0) {perror(recv failed);close(client_sock);continue;}buffer[bytes_received] \0; // 确保字符串结束// 转换小写为大写to_upper(buffer);// 发送数据回客户端if (send(client_sock, buffer, strlen(buffer), 0) 0) {perror(send failed);}// 关闭已处理的客户端连接close(client_sock);上述的代码是写在 while 循环中的表示对一个连接的处理 接下来将信息发送回客户端注意这时候已经是通过 client_socket 来发送数据了这个是在上面通过 accept() 函数来返回的 新 的 socket表示一个连接原本的 Hello Socket 仍然在 listen 状态等候连接 最后就是客户端接收返回的信息
客户端接收信息 // 接收服务器发来的数据ssize_t bytes_received recv(client_sock, buffer, BUFFER_SIZE - 1, 0);if (bytes_received 0) {perror(recv failed);} else {buffer[bytes_received] \0;printf(Received uppercase word from server: %s\n, buffer);}// 关闭客户端socketclose(client_sock);2.8.3 总结 相信看到这里大家对 socket 的执行流程已经有了大致的把握这里换个视角做一个总结。 其实整个流程就是 不断调用 socket API 来实现各种服务这体现了下层为上层通过接口提供服务的特点。
这里来回顾一下上面代码中使用到的 socket API
socket()函数用于创建一个新的socket参数指定了地址族AF_INET表示IPv4、套接字类型SOCK_STREAM表示TCP流式套接字和协议通常为0由系统选择合适的协议 用于上面 socket 的创建比如说客户端和服务器的 socket当这些信息都不指定的时候返回的就是一个普通的整型没有任何绑定这个绑定可以通过后续的 bind 和 connect 来额外附加最终使用的 socket 一定是绑定了客户端和服务器的全部信息的所以后续的一部分操作就是针对这个 socket 信息的修补来让它达到可以使用的状态 bind()绑定一个本地IP地址和端口到服务器socket。server_addr是一个初始化好的sockaddr_in结构体包含了服务器要监听的IP地址和端口号信息。 这就是服务器修补 socket 的方式将自己的端口号等信息补充到里面 listen()将服务器socket设置为监听模式并允许指定数量在例子中是10个的未完成连接请求排队等待。accept()该函数使服务器socket进入阻塞状态直到有新的客户端连接请求到达。成功建立连接后返回一个新的socket描述符client_sock用来与新客户端通信并通过client_addr获取客户端的地址信息。 这里的 socket 就是一个完完整整可以使用的 socket通过 accept 函数补充上了客户端的信息 connect()通常用于客户端程序连接到服务器端的 socket 这是客户端用来修补自己 socket 的方式调用后操作系统会自动分配临时端口信息 send() 和 recv()recv()用于从已连接的socket接收数据而send()则用于向已连接的socket发送数据。close()关闭已打开的socket文件描述符释放系统资源。
这就引出了另一种理解方式在真正的业务开始之前所有的操作都是为了使得通信双方获得 完整的 socket
客户端有了服务器的信息所以可以直接获取完整的 socket而服务器需要等待客户端发送信息给它才能获取完整的 socket所以服务器的 socket 会有 listen 的状态来等待信息补充完整。 补充三次握手 三次握手是TCPTransmission Control Protocol建立连接的过程确保了客户端和服务器之间的数据传输通道可靠、有序且无差错。第一次握手 客户端发送一个SYN同步序列编号Synchronize Sequence Numbers报文段到服务器其中包含客户端选择的一个初始序号seqx。此时客户端进入SYN_SENT状态。 第二次握手 服务器接收到客户端的SYN报文段后如果同意建立连接则回应一个SYNACK同步并确认报文段。这个报文段中包含了服务器的初始序号seqy并将确认号ack设置为x1表示已接收并期望接下来的数据从x1开始。服务器进入SYN_RECEIVED状态。 第三次握手 客户端收到服务器的SYNACK报文段后向服务器发送一个ACK确认报文段。该报文中确认号ack设置为y1表示已经接收到了服务器的SYN并且期望接下来的数据从y1开始。客户端同时也将自己的序号seq更新为x1。 以上面提到的视角看其实三次握手就是补充 socket 信息的一个过程