【计算机网络】网络编程套接字

news2024/11/29 6:25:06

文章目录

    • 理解源IP地址和目的IP地址
    • 理解端口号和进程ID
    • 理解源端口号和目的端口号
    • 认识TCP协议
    • 认识UDP协议
    • 网络字节序
    • socket编程接口
      • socket网址查看
      • socket常见API
      • UDP协议实现网络通信
        • UDP创建socket文件描述符
        • sockaddr结构
        • UDP绑定端口号
        • UDP接收发送网络数据
    • 简单的UDP网络程序
      • TCP协议实现网络通信
        • TCP创建socket文件描述符
        • TCP绑定端口号
        • TCP建立连接
        • TCP接收请求
        • TCP发起连接
        • TCP接收发送网络数据
    • 简单TCP网络程序
      • 多进程优化TCP服务器
      • 多线程优化TCP服务器
      • 线程池优化TCP服务器
    • socket编程总结
    • TCP协议三次握手四次挥手
    • 学习路线规划

理解源IP地址和目的IP地址

IP地址:公网IP,用于唯一标识互联网中的一台主机

源IP,目的IP:对于一个报文来讲,从哪来,到哪去。

源IP指将数据发送过来的IP地址,目的IP指将数据发送给下一个设备的IP地址(mac地址的变化)

意义: 指导一个报文该如何进行路径选择,目的IP是让我们根据目标进行路径选择的依据

理解端口号和进程ID

IP仅仅是解决了两台物理机器之间的相互通信,但是我们还要考虑如何保证双方的用户之间可以看到发送的和接受的数据。所以网络通信传输数据是使用进程来发送和接受的,pid唯一标识一台主机上的一个进程,port端口号也是唯一标识一台机器上的一个进程, 互联网世界本质上就是一个进程通信的世界

IP + PORT = socket(网络套接字)能够标识互联网中的唯一一个进程

端口号(port)是传输层协议的内容

  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据需要交给哪一个进程来处理
  • IP地址+端口号可以标识网络上某一台主机的某一个进程
  • 一个端口号只能被一个进程占用

进程PID vs PORT 为什么PID已经可以标识一台机器上的一个进程了,为啥还要创造PORT端口号呢??

这就像身份证号和学号的关系一样,身份证号和学号都可以唯一标识一名学生。若不使用学号只使用身份证号会出现以下问题:

1、如果身份证号的格式发生变化,那么学校的系统就不能用了

2、身份证号可能并不能标识一个学生在学校里的信息,比如入学年份、学院、专业、班级等,而只需要一个学号就可以获得这些信息,便于筛选查询数据

所以PORT的创建是为了网络部分和操作系统部分进行解耦

另外一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是描述"数据是谁发的,要发给谁"

计算机本身不产生数据,产生数据的是人。人通过特定的客户端产生数据通过网络通信被传送到服务器中。所以所谓的网络通信本质上就是进程间通信 ,比如:抖音的app客户端(进程)<->抖音的服务器(也是一个进程)

认识TCP协议

TCP(Transmission Control Protocol 传输控制协议)特性:

  • 传输层协议
  • 有连接
  • 不可传输
  • 面向数据报

认识UDP协议

UDP(USser Datagram Protocol 用户数据报协议)特性:

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

TCP 可靠,UDP不可靠。TCP需要使用更多的资源,具体使用哪个必须根据使用场景决定

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

为了使网络程序具有可移植性,使用同样的C代码在大端和小端计算机上编译后都能够正常运行,可以调用以下库函数做网络字节序和主机字节序转换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-003WDtyy-1673836199573)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230114135903412.png)]

这些函数名非常好记 h(host 主机) to n(net 网络) l(32位长整型) s(16位短整型)

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

如果主机是小端字节序,这些函数会将参数做相应的大小端转换然后返回

如果主机是大端字节序,这些函数不做转换,将参数原封不动的返回

socket编程接口

socket网址查看

PING + 网址 (查看IP + PORT)

socket常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)      
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

可以看到bind(), accept(), connect()函数的参数中都含有一个const struct sockaddr *address,这是因为网络通信的标准方式有多种,例如基于IP的网络通信,AF_INET,原始套接字,域间套接字等的通信方式。为了系统结构的统一化,程序员们设计出一种结构sockaddr作为一个参数标识通信方式,让我们可以使用同一个接口来完成通信

UDP协议实现网络通信

UDP创建socket文件描述符

man socket # 查看文档
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
DESCRIPTION
    // socket函数创建一个通信端点并且返回一个描述符
       socket() creates an endpoint for communication and returns a descriptor.
       The  domain  argument specifies a communication domain; this selects the protocol family which will be used for communication.  These families are defined in <sys/socket.h>.
    // 目前已知的具体格式包含
       The currently understood formats include:
Name                Purpose                          Man page
    //有点多就不全部复制过来了,最常用的就是这个
       AF_INET             IPv4 Internet protocols          ip(7)
    The socket has the indicated type, which specifies the communication semantics.  Currently defined types are:
	//TCP协议使用 提供有序,可靠,双向,基于连接的字节流,可以支持带外数据传输机制
       SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mechanism may be supported.
    //UDP协议使用 支持数据报(固定最大长度的无连接、不可靠消息)
       SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
RETURN VALUE     
    //成功返回文件描述符,若出现错误返回-1,并正确设置errno
       On success, a file descriptor for the new socket is returned.  On error, -1 is returned, and errno is set appropriately.
第一个参数:domain参数指定一个通信域,domain参数选择用于通信的协议族,这些协议族被定义在<sys/socket.h>头文件中
AF_INET             IPv4 Internet protocols          ip(7)

第二个参数:type 套接字有指定的类型,它指定通信语义,当前定义的类型有(这里也是列举了两个最常用的)
TCP协议使用 提供有序,可靠,双向,基于连接的字节流,可以支持带外数据传输机制
       SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mechanism may be supported.
UDP协议使用 支持数据报(固定最大长度的无连接、不可靠消息)
SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

第三个参数:protocol 指定协议,通常只有一个协议支持特定套接字类型,所以在这种情况下协议可以指定为零
返回值:成功返回一个文件描述符,失败返回-1

示例代码

// 1、创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0){
    std::cerr << "socket create errno:" << errno << std::endl;
    return 1; 
}

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.

在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址.

  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.

  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
    在这里插入图片描述

注意:

在socket API中sockaddr和sockaddr_un都可以使用sockaddr_in类型表示,当我们传入sockaddr_in类型的结构体对象指针,接口会拿出前16位地址类型判断使struct sockaddr还是structaddr_un。在使用的时候需要强制转化成sockaddr_in.

这里可能会有小伙伴感觉奇怪,为啥不使用void*传入参数而是从新创建一个结构体sockaddr_in呢?这是因为当时的C语言还并不支持 void *语法,为了方便两种接口的统一就这样设计的

UDP绑定端口号

想要使用绑定端口号函数首先要先初始化填充sockaddr结构体对象,需要填充三个参数

#include <sys/socket.h>

int bind(int socket, const struct sockaddr *address,
         socklen_t address_len);

第一个参数:socket      一个文件描述符,指定绑定的网络文件
第二个参数:address     一个结构体指针,用于指定协议类型
第三个参数:address_len ssize_t类型的重命名,用于指定address结构体对象的大小
返回值: 成功返回0,失败返回-1

查看address_in 类型的定义,编辑器用的是vscode,右击类型转到定义就可以查看了

struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

struct in_addr
  {
    in_addr_t s_addr;
  };
// 2 、服务器绑定端口和ip(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET;        //sockaddr 初始化1
//此处的端口号,是我们计算机上的变量,是主机序列
local.sin_port = htons(port); 	   //sockaddr 初始化2
//a.需要将人识别的点分十进制,字符串风格的IP地址,转换成4字节整数iP
//b.也要考虑大小端 
//云服务器不允许用户直接bind公网IP,另外,实际正常编写的时候也不会指明IP
//local.sin_addr.s_addr = inet_addr("43.2.2.2");
//INADDR_ANY: 如果你bind的是确定的IP(主机),意味着只有发到该IP主机上面的数据才会
//交给你的网络进程,但是一般的服务器右多个网卡,配置多个IP,我们需要的不是某个IP上的数据
//我们需要的是,所有发送到该主机,发送到该端口的数据
local.sin_addr.s_addr = INADDR_ANY;//sockaddr 初始化3
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind errno" << errno << std::endl;
return 2; 
}

UDP接收发送网络数据

UDP接受和发送网络数据一般使用这组接口。UDP使用的是数据报格式的数据传输,所以不可以使用字节流式接口

#include <sys/types.h>
#include <sys/socket.h>
// UDP发送网络数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// UDP接受网络数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

recvfrom()
第一个参数: sockfd 网络文件描述符
第二个参数: buf 传输的数据
第三个参数: len 传输数据的大小是多少个字节
第四个参数: flags 设置为0
第五个参数: *dest_addr 输出型参数,是谁给我这个进程发送数据的
第六个参数: addrlen 输出型参数用于标记dest_addr的大小
返回值 成功返回0 失败返回-1

sendto()
第一个参数: sockfd 网络文件描述符
第二个参数: buf 传输的数据
第三个参数: len 传输数据的大小是多少个字节
第四个参数: flags 设置为0
第五个参数: *dest_addr 我需要将这个数据发送给谁
第六个参数: addrlen 数用于标记dest_addr的大小
返回值 成功返回0 失败返回-1


示例代码

// 3、提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while (!quit){
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len); //网络中只有数据报和字节流,不需要传送'\0'
    if (cnt > 0){
        buffer[cnt] = 0;
        std::cout << "client# " << buffer << std::endl;
        std::string echo_hello = "hello client";
        sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);
    }
}
return 0;
}

简单的UDP网络程序

服务器代码

#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(std::string proc){
    std::cout << "Usage: " << proc << "port" << std::endl; 
}
int main(int argc, char* argv[]){
    if (argc != 2){
        Usage(argv[0]);
        return -1;
    }
    uint16_t port = atoi(argv[1]);
    // 1、创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0){
        std::cerr << "socket create errno:" << errno << std::endl;
        return 1; 
    }
    // 2 、服务器绑定端口和ip(特殊处理)
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    //此处的端口号,是我们计算机上的变量,是主机序列
    local.sin_port = htons(port); 
    //a.需要将人识别的点分十进制,字符串风格的IP地址,转换成4字节整数iP
    //b.也要考虑大小端 
    //云服务器不允许用户直接bind公网IP,另外,实际正常编写的时候也不会指明IP
    //local.sin_addr.s_addr = inet_addr("43.2.2.2");
    //INADDR_ANY: 如果你bind的是确定的IP(主机),意味着只有发到该IP主机上面的数据才会
    //交给你的网络进程,但是一般的服务器右多个网卡,配置多个IP,我们需要的不是某个IP上的数据
    //我们需要的是,所有发送到该主机,发送到该端口的数据
    local.sin_addr.s_addr = INADDR_ANY;
    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
        std::cerr << "bind errno" << errno << std::endl;
        return 2; 
    } 
    // 3、提供服务
    bool quit = false;
    #define NUM 1024
    char buffer[NUM];
    while (!quit){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        
        ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
        if (cnt > 0){
            buffer[cnt] = 0;
            std::cout << "client# " << buffer << std::endl;
            std::string echo_hello = "hello client";
            sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);

        }
    
    }
    return 0;
}

客户端代码

#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(std::string proc){
    std::cout << "Usage:\n\t" << proc << " server_ip server_port" << std::endl;
}

int main(int argc, char* argv[]){
    if (argc != 3) {
        Usage(argv[0]);
        return 3;
    }
    //1、创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0){
        std::cerr << "socket error : " << errno << std::endl;
        return 1; 
    }
    //2、客户端也必须显示的bind的吗?
    //a、首先,客户端必须也要有ip和port
    //b、但是,客户端不需要显示bind!一旦显示bind,就必须明确,client要和哪一个port关联
    //client指明端口号,在client一定存在吗??会不会已经被其他人绑定了呢??一旦port被占用则无法使用服务
    //server要求port必须明确,并且不变,但client只要有就可以!一般是OS自动给我们bind()
    //当client正常发送数据的时候,OS会自动给你bind,采用随机端口的方式,自动帮你匹配合适端口

    //2.使用服务
    while (true){
        std::string message;
        std::cout << "输入#";
        std::cin >> message;
        //a、你的数据从哪里来
        //b、你要发给谁
        sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(atoi(argv[2]));
        server.sin_addr.s_addr = inet_addr(argv[1]);
        sendto(sock, message.c_str(), message.length(), 0, (struct sockaddr*)&server, sizeof(server));
        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        char buffer[1024];
        int cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &len);
        if (cnt > 0){
            buffer[cnt] = 0;
            std::cout << "server echo#" << buffer << std::endl;
        }
       
    }
    return 0;
}

TCP协议实现网络通信

TCP创建socket文件描述符

客户端服务端都需要

//1.创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0){
	std::cerr << "socket create errno" << errno << std::endl;
	return 2;
}

TCP绑定端口号

服务端需要显示绑定,客户端OS自动绑定

//2.bind
uint16_t port = atoi(argv[2]);
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
local.sin_family = AF_INET;

if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
    std::cerr << "bind errno" << errno << std::endl;
    return 3;
}

TCP建立连接

服务端需要

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog); //第二个参数传5即可

阅读文档

DESCRIPTION //描述
//listen函数所指的sockfd套接字标记为被动套接字,就是这个套接字将使用accept(2)标准接收过来的连接请求
listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).
       The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.
       
//backlog参数定义sockfd的挂起连接队列可能增长的最大长度。如果连接请求在队列已满时到达客户端可能会收到带有ECONNREFUSED指示的错误,或者,如果底层协议支持重传,则可以忽略该请求,以便稍后重新尝试连接成功。
       The  backlog  argument defines the maximum length to which the queue of pending connections for sockfd may grow.  If a connection request arrives when the queue is full, theclient may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a  later  reattempt
at connection succeeds.

示例代码

//3.因为tcp是面向连接的,所以在通信之前必须要建立连接
//连接一定是有人主动建立(客户端),一定有人被动接收连接(服务器)
const int back_log = 5;
if (listen(sock, back_log) < 0){
	std::cerr << "listen error" << std::endl;
	return 4;
}

TCP接收请求

服务端需要

上文创建的套接字为监听套接字,用于与端口号绑定。accept()函数用于接收监听套接字获得到的请求,并与其建立连接,会返回一个新的文件描述符用于数据传输

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

阅读文档

accept()系统调用用于基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)。它提取侦听套接字挂起连接队列中的第一个连接请求sockfd,创建一个新的连接套接字,并返回一个引用该套接字的新文件描述符。新创建的套接字未处于侦听状态。原始套接字sockfd不受此调用的影响。

The  accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET).  It extracts the first connection request on the queue of pending connec‐tions for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket.  The newly created socket is not  in  the
listening state.  The original socket sockfd is unaffected by this call.

示例代码

//accept 
    for ( ; ; ){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len); //接收请求
        if (new_sock < 0){		//如果接收失败则跳过
            continue;   
        }
        //提供服务
        while (true){
        }
    }

TCP发起连接

客户端需要

struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
server.sin_family = AF_INET;
server.sin_port = htons(svr_port);
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
    std::cerr << "connect errno" << errno << std::endl;
    return 3;
}

TCP接收发送网络数据

因为TCP协议使用字节流的流式传输格式,所以也可以使用read(), write(),recv(), send()等方法流式写入读取的方法进行接收和发送数据

ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0){
    //将获取的内容当作字符串
    buffer[s] = 0;
    std::cout << "client# " << buffer << std::endl;
    std::string echo_string = ">>>server<<<";
    echo_string += buffer;
    echo_string += "...";
    write(sock, echo_string.c_str(), echo_string.size());
}

简单TCP网络程序

经过上面的学习,我们可以将示例代码进行拼接,很轻松就可以写出一个服务器和客户端

服务器代码

#include <iostream>
#include <cstring>
#include <cerrno>
#include <string>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

void Usage(std::string s){
    std::cout << "Usage :" << "\n\t" << s << " port " << std::endl;
}


int main(int argc, char* argv[]){
    if (argc != 2){
        Usage(argv[0]);
        return 1;
    }
    
    //1.创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock < 0){
        std::cerr << "socket create errno" << errno << std::endl;
        return 2;
    }
    std::cout << "listen success" << std::endl;
    //2.bind
    uint16_t port = atoi(argv[1]);
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_addr.s_addr = INADDR_ANY;
    local.sin_port = htons(port);
    local.sin_family = AF_INET;

    if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
        std::cerr << "bind errno" << errno << std::endl;
        return 3;
    }
    std::cout << "bind success" << std::endl;

    //3.因为tcp是面向连接的,所以在通信之前必须要建立连接
    //连接一定是有人主动建立(客户端),一定有人被动接收连接(服务器)
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0){
        std::cerr << "listen error" << std::endl;
        return 4;
    }
    //accept 
    for ( ; ; ){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if (new_sock < 0){
            continue;   
        }
        std::cout << "get a link ..." << std::endl; 
            //提供服务
        while (true){
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
            if (s > 0){
                //将获取的内容当作字符串
                buffer[s] = 0;
                std::cout << "client# " << buffer << std::endl;
                std::string echo_string = ">>>server<<<";
                echo_string += buffer;
                echo_string += "...";
                write(sock, echo_string.c_str(), echo_string.size());
            }
            else if (s == 0){
                std::cout << "client quit ..." << std::endl;
                break;
            }
            else {
                std::cerr << "read errno" << errno << std::endl;
                break;
            }
        }
    }
}

客户端代码

#include <iostream>
#include <stdlib.h>
#include <cerrno>
#include <cstring>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void Usage(std::string s){
    std::cout << "Usage :" << "\n\t" << s << " server_ip server_port " << std::endl;
}
int main(int argc, char* argv[]){
    if (argc != 3){
        Usage(argv[0]);
        return 1;
    }
    std::string svr_ip = argv[1];
    uint16_t svr_port = atoi(argv[2]);
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0){
        std::cerr << "socket errno" << errno << std::endl;
        return 2;
    } 

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    server.sin_family = AF_INET;
    server.sin_port = htons(svr_port);
    if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
        std::cerr << "connect errno" << errno << std::endl;
        return 3;
    }

    //进行正常的业务请求
    while (true){
        std::cout << "Please Enter# ";
        char buffer[1024];
        fgets(buffer, sizeof(buffer) - 1, stdin);
        write(sock, buffer, strlen(buffer));

        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0){
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
}

但是以上代码有一个非常明显的问题,那就是一个服务器在接收一个客户端的连接后为这个客户端提供服务,而主进程就死循环执行服务代码了,无法继续接收其他客户端的连接,导致我们的服务器在同一时刻只能服务一个客户端,接下来我们会使用操作系统的知识对服务器进行优化

多进程优化TCP服务器

我们可以让服务器主进程接收客户端的连接,然后创建子进程来为客户端提供服务。

方法一 在信号章节我们学过signal()函数,其可以自定义信号的处理方式

#include <signal.h>
signal(SIGCHLD, SIG_IGN); // 在Linux中父进程忽略子进程的SIGCHILD信号,子进程会自动退出释放资源

在子进程退出时会给父进程发送SIGCHILD信号,告诉父进程我推出了,如果我们让父进程忽略SIGCHILD信号,那么子进程就会自动退出并且释放资源,父进程就无需等待

方法二 让父进程进行waitpid(),子进程马上退出,让孙子进程执行服务,孙子进程会被操作系统领养

//child 
if (fork() > 0) exit(0);
close(listen_sock);
ServiceIO(new_sock); //提供服务
close(new_sock);
exit(0);

所以我们可以将提供的服务放在一个函数中供子进程调用

signal(SIGCHLD, SIG_IGN); // 在Linux中父进程忽略子进程的SIGCHILD信号,子进程会自动退出释放资源
    //accept 
    for ( ; ; ){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if (new_sock < 0){
            continue;   
        }
        std::cout << "get a link ..." << std::endl; 
        pid_t id = fork();
        if (id  < 0){
            continue;
        }
        else if (id == 0){
            //child 
            if (fork() > 0) exit(0);
            close(listen_sock);
            ServiceIO(new_sock); //提供服务
            close(new_sock);
            exit(0);
        }
        else {
            //father
            close(new_sock);
        }
    }

子进程会继承父进程的struct files_struct因为子进程只提供服务所以listen_sock是没有用的,可以将其关掉,对于父进程只用于监听接收请求,所以提供服务的new_sock传递给子进程后就没有用了,可以关闭,防止文件描述符泄露

多线程优化TCP服务器

创建线程

pthread_t tid;
int *pram = new int(new_sock);
pthread_create(&tid, nullptr, HandlerRequest, pram);

线程执行函数

void *HandlerRequest(void* args){
    pthread_detach(pthread_self());
    int sock = *(int*)args;
    delete (int*)args;
    ServiceIO(sock);   	//提供服务
    close(sock);
}

注意:该代码存在以下问题

a、创建线程、进程无上限

b、当客户连接来了,我们才给客户创建进程/线程

线程池优化TCP服务器

使用find()指令查找我们之前写过的线程池

[clx@VM-20-6-centos Lesson_Linux]$ ll
total 28
drwxrwxr-x 21 clx clx 4096 Aug  1 00:49 21_7_lesson
drwxrwxr-x 25 clx clx 4096 Aug 27 09:40 21_8_lesson
drwxrwxr-x 21 clx clx 4096 Oct 30 20:28 22_10_lesson
drwxrwxr-x 23 clx clx 4096 Nov 28 19:13 22_11_lesson
drwxrwxr-x  6 clx clx 4096 Dec 30 21:09 22_12_lesson
drwxrwxr-x 18 clx clx 4096 Sep 30 18:01 22_9_lesson
drwxrwxr-x  5 clx clx 4096 Jan 16 09:01 23_1_lesson
[clx@VM-20-6-centos Lesson_Linux]$ find . -name ThreadPool.hpp //查找线程池
./22_11_lesson/lesson_11_24/FirstProject/ThreadPool.hpp
./22_11_lesson/lesson_11_25/FirstProject/ThreadPool.hpp

创建服务套接字后将使用套接字创建一个任务,然后Push到线程池里就可以了

Task t(new_sock);
ThreadPool::GetInstance()->InitThreadPool();
ThreadPool::GetInstance()->PushTask(new_sock);

Task代码

#pragma once 
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

class Task{
public:
    Task() : sock(-1){};
    Task(int _sock) : sock(_sock){}
    void ProcessOn(){
        // while (true){
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
            if (s > 0){
                //将获取的内容当作字符串
                buffer[s] = 0;
                std::cout << "client# " << buffer << std::endl;
                std::string echo_string = ">>>server<<<";
                echo_string += buffer;
                echo_string += "...";
                write(sock, echo_string.c_str(), echo_string.size());
            }
            else if (s == 0){
                std::cout << "client quit ..." << std::endl;
                //break;
            }
            else {
                std::cerr << "read errno" << errno << std::endl;
                //break;
            }
        //}
        close(sock);
    }    
    void operator()(){
        ProcessOn();
    }
private:
    int sock;
};

线程池代码

#pragma once
#include <iostream>
#include <pthread.h>
#include <queue>
#include "Task.hpp"

#define THREAD_NUM 6

class ThreadPool
{
private:
  ThreadPool(int num = THREAD_NUM) : _pthread_num(num), _stop(false)
  {
    pthread_mutex_init(&_lock, nullptr);
    pthread_cond_init(&_cond, nullptr);
  }

public:
  ~ThreadPool()
  {
    pthread_mutex_destroy(&_lock);
    pthread_cond_destroy(&_cond);
  }

  static void *ThreadRoutine(void *args);
  static ThreadPool *GetInstance();
  bool InitThreadPool();
  void PushTask(const Task &task);
  void PopTask(Task &task);

  void ThreadWait() { pthread_cond_wait(&_cond, &_lock); }
  void ThreadWakeUp() { pthread_cond_signal(&_cond); }
  void ThreadLock() { pthread_mutex_lock(&_lock); };
  void ThreadUnlock() { pthread_mutex_unlock(&_lock); };

  bool IsStop() { return _stop; }
  bool TaskQueueIsEmpty() { return _task_queue.empty(); }

private:
  std::queue<Task> _task_queue;
  size_t _pthread_num;
  bool _stop;
  pthread_mutex_t _lock;
  pthread_cond_t _cond;
  static ThreadPool *_single_instance;
};
ThreadPool *ThreadPool::_single_instance = nullptr;

ThreadPool *ThreadPool::GetInstance()
{
  static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  if (_single_instance == nullptr)
  {
    pthread_mutex_lock(&mutex);
    _single_instance = new ThreadPool();
    _single_instance->InitThreadPool();
    pthread_mutex_unlock(&mutex);
  }
  return _single_instance;
}

bool ThreadPool::InitThreadPool()
{
  pthread_t tid;
  for (size_t i = 0; i < _pthread_num; i++)
  {
    int ret = pthread_create(&tid, nullptr, ThreadRoutine, (void *)this);
    if (ret != 0)
    {
      std::cout << "pthread_create errno" << std::endl;
      return false;
    }
  }
  std::cout << "InitThreadPool success" << std::endl;
  return true;
}

void *ThreadPool::ThreadRoutine(void *args)
{
  ThreadPool *tp = (ThreadPool *)args;
  while (true)
  {
    Task task(0);
    tp->ThreadLock();
    while (tp->TaskQueueIsEmpty())
    {
      tp->ThreadWait();
    }
    tp->PopTask(task);
    tp->ThreadUnlock();
    task.ProcessOn();
  }
}

void ThreadPool::PushTask(const Task &task)
{
  ThreadLock();
  _task_queue.push(task);
  ThreadWakeUp();
  ThreadUnlock();
}
void ThreadPool::PopTask(Task &task)
{
  task = _task_queue.front();
  _task_queue.pop();
}

socket编程总结

  • 如果不关闭不需要的文件描述符,会造成文件描述符泄露
  • 创建socket的过程,socket()本质是打开文件 – 仅仅有系统相关的内容
  • bind(), struct sockaddr_in -> ip, port 本质使用ip + port 和文件信息进行关联
  • listen(), 本质是设置该socket文件的状态,允许别人来连接我
  • accept(), 获取新链接到应用层,是以fd为代表的。当有很多链接连上我们的服务器的时候,OS中会存在大量链接。OS许哟啊管理这些建立好的链接,如何管理??先描述再阻止
  • 所谓的链接,在OS层面上本质就是一个描述链接的结构体(文件)
  • read/write,本质就是进行网络通信,但是对于用户来讲,相当于文件读写
  • close(fd)关闭文件 a.系统层面,释放曾经申请的文件资源,链接资源等 b.网络层面,通知对方,我的丽娜姐已经关闭了
  • connect(),本质就是发起连接,在系统层面,就是构建一个请求报文发送过去,在网络层面,发起TCP连接的三次握手
  • close(), client 和 server都需要执行,本质在网络层面,其实就是在进行第四次挥手

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hXnDEdo7-1673836199576)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230116101425614.png)]

TCP协议三次握手四次挥手

服务器初始化:

  • 调用socket, 创建文件描述符;

  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失

败;

  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;

  • 调用accecpt, 并阻塞, 等待客户端连接过来;

建立连接的过程:

  • 调用socket, 创建文件描述符;

  • 调用connect, 向服务器发起连接请求;

  • connect会发出SYN段并阻塞等待服务器应答; (第一次)

  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)

  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

这个建立连接的过程, 通常称为 三次握手;

数据传输的过程

  • 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;

  • 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;

  • 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;

  • 客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程:
这个断开连接的过程, 通常称为四次挥手

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);

  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);

  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)

  • 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

在学习 socket API 时需要注意应用程序和TCP协议层是如何交互的:

应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段

应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些字段,再比如read()返回0就表明收到了FIN段

学习路线规划

现在我们从零开始,通过系统调用接口(socket, bind, listen …)来编写应用层,在完善应用层的基础上之后我们会学习操作系统的传输层和网络层,还有网卡驱动中的数字链路层的原理以及设计
接请求;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/166256.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

拉伯证券|大股东或易主,阿里巴巴换股入局

三大股指齐上扬&#xff0c;早盘主力埋伏这些股。 到午间收盘&#xff0c;“家居零售榜首股”之称的美凯龙一字涨停&#xff0c;港股红星美凯龙涨24%&#xff0c;此前一度涨超30%。 消息面上&#xff0c;1月13日晚间&#xff0c;美凯龙发布公告称&#xff0c;公司控股股东红星…

transformer算法解析

本文参考&#xff1a; 详解Transformer &#xff08;Attention Is All You Need&#xff09; - 知乎 Transformer 代码完全解读&#xff01;_AI科技大本营的博客-CSDN博客 Transformer学习笔记一&#xff1a;Positional Encoding&#xff08;位置编码&#xff09; - 知乎 1、…

【C语言】自定义类型

前言男孩子在外面要保护好自己~一、结构体为什么会有结构体呢&#xff1f;但要描述一个复杂对象时&#xff0c;仅用之前学过的基本数据类型表达不了&#xff08;如&#xff1a;我要描述一个人&#xff0c;仅靠基本数据类型只能说定义他的一种属性<如用 int 定义他的年龄>…

字符串的处理

一、字符数组 用来存放字符型数据的数组称为字符数组&#xff0c;其元素是一个个的字符。 char 字符数组名[常量表达式]&#xff1b; C语言规定字符串是以\0字符作为结束符的字符数组。 在程序中可以通过判断数组元素是否为空字符来判断字符串是否结束。 字符串的输…

介绍Java中的常/变量.各种数据类型以及类型转换和提升的用法

本文简单描述了什么是常量和变量,介绍了Java各种数据类型:基本数据类型(四类八种,大小和范围)和引用数据类型(种类),简单介绍了包装类字符串类型,以及不同数据类型之间的常量和变量,数据类型之间的转换和提升… Java常/变量和数据类型一.什么是常量?二.什么是变量?三.数据类型…

[审核]因为审核人员不了解苹果登录被拒

1.审核被拒信息 Guideline 2.1 - Information Needed We’re looking forward to completing the review of your app, but we need more information to continue. Next Steps Please provide detailed answers to the following questions in your reply to this message i…

寒假集训一期总结(一)–––思维训练

目录 思维训练 走方格 解题思路 参考代码 最短曼哈顿距离 ​编辑 解题思路 参考代码 酒厂选址 解题思路 参考代码 雪地足迹Tracks in the Snow 解题思路 参考代码 一个星期没有更博客了…这一个星期,去学校信竞集训的我收获颇丰,下面就是我的还加集训总结 思…

【小白向】让电脑成为热点WIFI

让电脑成为热点WIFI 本文针对下述情况&#xff0c;有一台电脑&#xff0c;一部手机&#xff0c;但是电脑通过网线连接。此时电脑可以上网&#xff0c;手机没有流量&#xff0c; 仅能通过WIFI上网&#xff0c;但此时没有WIFI。 其实你的电脑可能自己本身就能作为热点发布WIFI&…

绕任一向量旋转矩阵计算思考与实现

欢迎关注更多精彩 关注我&#xff0c;学习常用算法与数据结构&#xff0c;一题多解&#xff0c;降维打击。 问题提出 如图所示&#xff0c;在空间中有一向量A&#xff0c;问点O绕A方向逆时针旋转角度α的矩阵如何表示。 问题分析 问题化规 直接去构造一个矩阵是比较困难的。…

自从我学会了Jenkins的自动构建,我再也没有每次都打包上传到服务器然后发布Java服务了

上次我们讲了使用Jenkins部署maven项目 工作两年半&#xff0c;终于学会了Jenkins部署Maven项目 这次我们来讲一下每次提交代码的时候Jenkins自动构建 我们使用的代码仓库是gitee 文章目录&#x1f3c3;第一步&#xff0c;我们在Jenkins中安装gitee插件&#x1f3c3;第二步&am…

Go语言并发编程及依赖管理

目录&#x1f9e1;并发编程GoroutineCSP(Communicating Sequential Processes)&#x1f9e1;依赖管理依赖演变依赖管理三要素&#x1f49f;这里是CS大白话专场&#xff0c;让枯燥的学习变得有趣&#xff01; &#x1f49f;没有对象不要怕&#xff0c;我们new一个出来&#xff0…

Linux (open、write、read、close、lseek、chmod、sync)操作文件的函数详解

目录 一、文件操作方式 二、Linux底层文件操作 1. open 2. write 3. read 4. close 5. lseek 6. chmod 7. sync、syncfs、fsync、fdatasync 三、 Linux 系统调用 四、总结 linux中&#xff0c;一切皆文件&#xff08;网络设备除外&#xff09; 硬件设备也“是”文件&a…

力扣刷题记录——507.完美数、509. 斐波那契数、520. 检测大写字母

本专栏主要记录力扣的刷题记录&#xff0c;备战蓝桥杯&#xff0c;供复盘和优化算法使用&#xff0c;也希望给大家带来帮助&#xff0c;博主是算法小白&#xff0c;希望各位大佬不要见笑&#xff0c;今天要分享的是——《507.完美数、509. 斐波那契数、520. 检测大写字母》。 目…

InfluxDB + Grafana计算成功率

文章目录方式一 借助Grafana的Transfrom方式二 Influx子查询Transfrom介绍建议针对每类Metric&#xff0c;使用一个Metric&#xff0c;增加success的tag区分成功还是失败。 方式一 借助Grafana的Transfrom 第一步&#xff1a;新建2个Query Query Total: SELECT sum("coun…

安科瑞电气火灾监控系统在春晓161#地块人防工程的设计与应用

安科瑞 华楠摘要&#xff1a;本文简述了电气火灾监控系统的组成原理&#xff0c;分析了电气火灾监控系统在应用中的设计依据和相关规范。通过安科瑞剩余电流式电气火灾监控系统在春晓161#地块人防工程电气火灾监控系统项目的实例介绍&#xff0c;阐述了电气火灾监控系统功能的实…

c语言实现扫雷(详细讲解)

本篇介绍,讲解如何使用c语言实现扫雷小游戏. 金句分享: ✨✨✨爱你所爱,行你所行✨✨✨ 目录前言:一、游戏设计思路介绍:效果展示二、游戏的分步讲解2.1、主函数测试区&#xff08;test.c&#xff09;基本构成2.2、游戏中函数实现区(game.c) (重点)2.21、雷盘的创建与初始化函…

centos8 Ambari-2.7.6.3+HDP-3.3.1离线安装详细教程(附安装包)

自2021年1月31日开始,所有Cloudera软件都需要有效的订阅,且订阅费昂贵。此外,CDH6和HDP3将是CDH和HDP的最后企业版本,原有企业版用户无法继续获取新的功能和性能提升。至2022年3月份,CDH/HDP全部停止服务(EoS),用户没办法获取售后支持。由于生产环境系统升级到centos8,…

linux 中 PCIE 中断映射机制

PCIE 中断映射机制 1、 PCIE 中有三种中断方式&#xff0c; MSI&#xff0c;MSI-X 和INTx PCIe总线继承了PCI总线的所有中断特性&#xff08;包括INTx和MSI/MSI-X&#xff09;&#xff0c;以兼容早期的一些PCI应用层软件。 PCI总线最早采用的中断机制是INTx&#xff0c;这是…

基于Flink+kafka实时告警

引出问题 项目使用告警系统的逻辑是将实时数据保存到本地数据库再使用定时任务做判断&#xff0c;然后产生告警数据。这种方式存在告警的延时实在是太高了。数据从产生到保存&#xff0c;从保存到判断都会存在时间差&#xff0c;按照保存数据定时5分钟一次&#xff0c;定时任务…

智慧水务能效管理平台在污水处理厂电气节能中的应用

摘要&#xff1a;污水处理属于高能耗行业&#xff0c;会消耗大量的电能、燃料和药剂等&#xff0c;高能耗不仅会提升污水处理成本&#xff0c;还会加剧能源危机。所以&#xff0c;本文首先探究了污水处理厂耗能的原因&#xff0c;分析了污水处理与节能降耗的关系&#xff0c;然…