网络编程套接字

文章目录

  • 一、理解源IP和目的IP
  • 二、认识端口号
  • 三、理解port端口号和进程ID
  • 四、理解IP和port端口号
  • 五、理解源端口号和目的端口号
  • 六、认识TCP协议
  • 七、认识UDP协议
  • 八、网络字节序
    • 字节处理函数
  • 九、socket编程
    • socket的解释
    • socket常见API
    • sockaddr结构
  • 十、UDP套接字通信
  • 十一、TCP套接字通信
      • 版本1:单进程版
      • 版本2:多进程版本
      • 版本2.1:在版本2的基础上加深
      • 版本3:线程库版
      • 版本4:线程池版
  • 总结

一、理解源IP和目的IP

这里的IP指的是 :公网IP
用来唯一的表示互联网中的一台主机
源IP:对于一个报文来讲,从哪里来
目的IP:对于一个报文来讲,去往哪里
最大的意义:指导一个报文如何进行路径选择,到哪里去本质就是让我们根据目标,进行路径的选择

二、认识端口号

数据从主机A到达主机B不是目的,而是到达主机B上的一个进程,提供进行处理服务。

数据刚开始的时候,是从哪里来的?

计算机本身不产生数据,产生数据的是人!而人是通过特定的客户端产生的数据。
本质上:所有的网络通信,站在普通人的角度,都是人和人之间的通信;站在技术人员的角度,所有的网络通信本质是进程和进程之间的通信。

IP仅仅解决了两个物理机器之间的相互通信,但是我们还要考虑如何保证双方用户之间能够看到发送和接收的数据呢?

所以我们需要端口号。
我们知道,在单个计算机中的进程是用进程标识符(PID)来标识的,但是在互联网环境下,用计算机操作系统所指派的这种进程标识符来标识运行在应用层的各种应用进程则是不行的,这是因为在互联网上使用的计算机操作系统种类很多,不同的操作系统又使用不同格式的进程标识符,为了使运行不同操作系统的计算机的应用进程能够互相通信,就必须使用统一的方法,对TCP/IP 体系的应用进程进行标志。

端口号的作用:唯一的标识了一台主机上唯一的一个进程,端口号只具有本地意义,它只是为了标志本计算机应用层中各个进程在和运输层交互时的层间接口,在互联网不同计算机中,相同的端口号是没有关联的。

所以:IP+PORT就能唯一标识互联网的唯一的一个进程!!!!整个网络看做是一个大的OS,所有的网络上网行为,基本都是在这一个大的OS内,进行进程间通信!

IP地址+PORT端口号= socket

三、理解port端口号和进程ID

类比学习:每个同学都是有身份证号的(PID),同时在学校的时候也是有学号的(PORT)。
如果学校也只用身份证来区分同学,也是可行的,但是这样也只是进行了唯一性区分,并不方便对于同学们的管理(并不能知道同学的专业、班级、学院等)。
使用学号的最大的好处:不受外部影响,不用作任何的变化,而且方便管理。

一个进程可以绑定多个端口号吗??——可以
一个端口号可以绑定多个进程吗??——不可以

四、理解IP和port端口号

要进行通信的本质:
1、先找到目标主机
2、在找到该主机上的服务(进程)

小结一下:互联网的世界,本质就是进程间通信的世界!
进程具有独立性,进程间通信的前提工作:得先让不同的进程,看到同一份资源!这个资源在这里就是指的网络!

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

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

六、认识TCP协议

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

七、认识UDP协议

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

八、网络字节序

不同CPU保存和解析数据的方式不同(主流的Intel系列CPU为小端字节序),显然小端字节序系统和大端字节序系统通信时会发生数据解析错误,因此在网络编程中,发送数据前需要将数据转化为统一的格式——网络字节序(Network Byte Order),网络字节序统一为大端字节序。
网络编程套接字_第1张图片

字节处理函数

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。套接字地址结构仅仅供本机TCP协议记录套接字信息而用,这个结构体变量本身是不在网络上面进行传输的,但是它的某些内容,如IP地址和端口号是在网络上进行传输的,这也是为什么这两部分数据需要转换成为网络字节序的原因。

#include 
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回

除了以上转换端口的函数外,还需要将字符串转换为网络字节序,完成点分十进制IP地址与二进制IP地址之间的相互转化:

// 字符串转为in_addr
int  inet_aton(const char* strptr,struct in_addr * addrptr);
in_addr_t inet_addr(const char* strptr);

// in_addr转为字符串
char* inet_ntoa(struct in_addr inaddr);

九、socket编程

socket的解释

本来socket的意思就是插座的意思。选用socket这个名词是相当准确的,其实一条TCP连接就像一条电缆线,其两端都各带有一个插头。把每一端的插头插入位于主机的应用层和运输层之间的插座后,两个主机之间的进程就可以通过这条电缆线进行通信了。但是插座这个名词很容易让人想起硬件,而socket是个软件名词,这样“套接字”就成为标准译名了。

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);
函数名称 socket
函数功能 创建一个新的套接字
头文件 #include
#include
函数原型 int socket(int domain , int type,int protocol);
参数 domain:套接字的通信域
type:指定套接字的类型
protocol: 与该套接字一起使用的特定协议
返回值 返回一个文件描述符:成功
-1:失败,且错误码errno被设置
函数名称 bind
函数功能 将一个套接字绑定到一个地址上,使得客户端可以连接
头文件 #include
函数原型 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 sockfd:上一个socket( )函数调用中获得的文件描述符
addr:指定包含绑定地址的结构的指针
addrlen:地址结构体的大小
返回值 0:成功
-1:失败,errno变量被设置
函数名称 listen
函数功能 在一个监听socket上接收一个连接,并返回对等的socket地址
头文件 #include
函数原型 int listen(int sockfd, int backlog);
参数 socket:套接字系统调用中获得的文件描述符
backlog:sockfd挂起的已连接队列可以增长的最大长度(这个参数后面会详细解释)
返回值 0:成功
-1:失败
函数名称 accept
函数功能 允许一个套接字接收来自其他套接字的接入连接
头文件 #include
函数原型 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数 sockfd:socket()系统调用中获取的文件描述符,这是一个监听套接字
addr:指向保存连接对端的地址,即客户端地址
addrlen:指定地址结构的大小
返回值 成功返回一个文件描述符,这是一个提供IO服务的套接字,-1:失败
函数名称 connect
函数功能 建立与另一个套接字之间的连接
头文件 #include< sys/socket.h>
函数原型 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 socket:socker()系统调用中获得的文件描述符
addr:指向的是服务器结构体的指针
addrlen:指定地址结构的大小
返回值 0:成功
-1:失败,errno变量被设置

sockaddr结构

网络通信的标准方式有很多种,基于ip的网络通信——IP套接字(设置为AF_INET),原始套接字,域间套接字等,所以为了方便:系统结构统一化,就定义了sockaddr结构体来表示一个通用结构体

struct sockaddr
{
	uint8_t sa_len;
	sa_familiy_t sa_family;
	char sa_data[14];	// 将ip和port表示在一起
};

网络编程套接字_第2张图片
使用时需要将sockaddr_in强转成sockaddr类型,因此sockaddr是通用的标准定义的,而sockaddr_in是IP协议实现的,通常需要将sockaddr_in转换成sockaddr传递给某些网络编程函数。

十、UDP套接字通信

sendtorecvfrom函数用于在无连接的数据包套接字方式下,进行数据的传输。由于本地套结字没有与远程机器建立连接,因此在发送数据时,应该指明目的地址。

函数名称 sendto
函数功能 向UDP连接的另一端发送数据
头文件 #include
#include
函数原型 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
参数 sockfd:指定发送端套接字描述符
buf:指向发送数据的指针
len:数据的长度
flags:一般置为0
dest_addr:struct sockaddr类型变量的目的IP地址和目的端口号信息
addrlen:内存区的地址长度,一般为sizeof(struct sockaddr_in)
返回值 实际发送的字节数:成功
-1:失败
函数名称 recvfrom
函数功能 从UDP连接的另一端接收数据
头文件 #include
#include
函数原型 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
参数 sockfd:指定接收端套接字描述符
buf:要读取信息的缓冲区
len:缓冲的最大长度
flags:一般置为0
src_addr:struct sockaddr类型的变量,保存源IP和源端口号
addrlen实际存取src_addr的数据字节数
返回值 实际发送的字节数:成功
-1:失败

网络编程套接字_第3张图片
在网络通信中,只有报文大小,或者是字节流中字节的个数,没有C/C++字符串这样的概念(虽然我们后续可能经常遇到)

udp_server.cc

#include 
#include 
#include 
#include 
#include 
#include  /* struct sockaddr_in*/
#include 
const uint16_t port = 8080;
int main()
{
    // 1.创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket create error!" << errno << std::endl;
        return 1;
    }

    // 2.给该服务器绑定端口和IP(特殊处理)
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port); // 此处的端口号,是我们计算机上的变量,是主机字节序列,需要转成网络字节序

    // a.需要将人识别的点分十进制-->字符串风格,转化成为4字节整数的IP(2进制)
    // b.也要考虑到字节序列问题
    // in_addr_t inet_addr(const char *cp);等价于ab,用此函数就可以完成字符串转换成为网络字节序
    // 坑:云服务器,不允许用户直接bind公网IP,另外,实际正常编写的时候,我们也不会指明IP
    // local.sin_addr.s_addr=inet_addr("119.91.227.98");

    local.sin_addr.s_addr = INADDR_ANY; // 0

    if (bind(sock, (sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cout << "bind error:" << errno << std::endl;
        return 2;
    }

    // 3.提供服务
#define NUM 1024
    char buffer[NUM];
    bool quit = false;
    while (!quit)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len);
        std::cout << "client# " << buffer << std::endl;

        std::string echo_hello = "hello";
        sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (sockaddr *)&peer, len);
    }
    return 0;
}

作为一个服务器,要不要让客户知道,对应的服务器的地址(ip+port)?

 必须的!!!服务器的socket信息(ip+port),必须得被客户知道!

INADDR_ANY的含义

如果你bind的是确定的IP(主机),意味着只有发到该IP主机上面的数据,才会交给你的网络进程,但是,一般服务器可能有多张网卡,配置多个IP,我们需要的不是,某个IP上面的数据,我们需要的是,所有发送到该主机,发送到该端口的数据!

udp_client.cc

#include 
#include 
#include 
#include 
#include 
#include  /* struct sockaddr_in*/
#include 
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 0;
    }
    // 1.创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket create error!" << errno << std::endl;
        return 1;
    }

    // 2.使用服务
    // b.你要发给谁?
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    while (true)
    {
        // a.数据从哪里来?
        std::string message;
        std::cout << "输入# ";
        std::cin >> message;

        sendto(sock, message.c_str(), message.size(), 0, (sockaddr *)&server, sizeof(server));

        //此处天tmp就是一个 占位符
        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        char buffer[1024];
        recvfrom(sock, buffer, sizeof(buffer), 0, (sockaddr *)&tmp, &len);
        std::cout<<"server echo# "<<buffer<<std::endl;
    }

    return 0;
}

客户端需要显式的bind的吗?

a首先,客户端必须也要有ip和port
b但是,客户端不需要显示的bind!一旦显式bind,就必须明确,client要和哪一个port关联
client指明的端口号,在client端一定会有吗?有可能被占用,被占用导致client无法使用
server要的是port必须明确,而且不变,但client只要有就行!

所以一般是OS自动给你bind,就是client正常发送数据的时候,OS自动给你bind!采用的是随机端口的方式!

我们想要实现新的业务逻辑:客户端发出命令请求,并且执行该命令,服务器给予一定的反馈

函数名称 popen()
函数功能 popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令
头文件 #include
函数原型 FILE * popen( const char * command,const char * type);
参数 参数type可使用“r”代表读取,“w”代表写入
返回值 依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针,若成功则返回文件指针,否则返回NULL,错误原因存于errno中

需要使用的两个函数:

#include 
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);

udp_server.cc

#include 
#include 
#include 
#include 
#include 
#include 
#include  /* struct sockaddr_in*/
#include 

// const uint16_t port = 8080;

// ./udp_server port
std::string 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 error!" << errno << std::endl;
        return 1;
    }

    // 2.给该服务器绑定端口和IP(特殊处理)
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port); // 此处的端口号,是我们计算机上的变量,是主机字节序列,需要转成网络字节序

    // a.需要将人识别的点分十进制-->字符串风格,转化成为4字节整数的IP
    // b.也要考虑到字节序列问题
    // in_addr_t inet_addr(const char *cp);等价于a+b,用此函数就可以完成字符串转换成为网络字节序
    // 坑:云服务器,不允许用户直接bind公网IP,另外,实际正常编写的时候,我们也不会指明IP
    // local.sin_addr.s_addr=inet_addr("119.91.227.98");

    local.sin_addr.s_addr = INADDR_ANY; // 0

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

    // 3.提供服务
#define NUM 1024
    char buffer[NUM];
    bool quit = false;

    // Xshell
    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; // 0=='\0'
            FILE *pf = popen(buffer, "r");
            std::string echo_hello;
            char line[1024]={0};
            while (fgets(line, sizeof(line), pf) != NULL)
            {
                echo_hello += line;
            }

            // if(feof(pf)) 用于判断是否读取完成
            // {}
            pclose(pf);

            // 根据用户输入,构建新字符串
            std::cout << "client# " << buffer << std::endl;
            sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr *)&peer, len);
        }
        else
        {
        }
    }
    return 0;
}

udp_client.cc

#include 
#include 
#include 
#include 
#include 
#include 
#include  /* struct sockaddr_in*/
#include 
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 0;
    }
    // 1.创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket create error!" << errno << std::endl;
        return 1;
    }

    // 2.使用服务
    // b.你要发给谁?
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    while (true)
    {
        // a.数据从哪里来?
        // std::string message;
        // std::cout << "输入# ";
        // std::cin >> message;
        std::cout << "Myshell % ";
        char line[1024];
        fgets(line, sizeof(line), stdin);
        sendto(sock, line, strlen(line), 0, (sockaddr *)&server, sizeof(server));

        //此处天tmp就是一个 占位符
        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        char buffer[1024];
        ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&tmp, &len);
        if (cnt > 0)
        {
            buffer[cnt] = 0;
            std::cout << buffer << std::endl;
        }
        else
        {
        }
    }

    return 0;
}

网络编程套接字_第4张图片

十一、TCP套接字通信

网络编程套接字_第5张图片

因为TCP是面向连接的,意思就是通信之前先要建立连接(比如打电话,必须要拨通电话,才能通信一个道理)所以有两步:1️⃣先要建立连接 2️⃣然后再通信

所以一定有人主动建立连接(client主动建立连接,需要服务),一定有另外的被动接收连接(server接收连接,提供服务)!!
我们当前写的是一个server服务器,一定会周而复始的等待客户连接的到来,所以我们要不断的提供一个建立连接的功能——设置套接字是Listen状态,本质是允许用户连接。

因为TCP是面向字节流的,所以我们可以像文件一样进行读取操作。

client需不需要显式绑定?

bind时不需要显式的bind,永远都是客户端去连接服务器,客户端最关心的是connect

inet_addr做了两件事情:
1️⃣将点分十进制的字符创风格的IP(“119.91.227.98”),转换成为4字节的IP(就是一个整数)。
2️⃣将主机序列转换成为网络字节序列。

版本1:单进程版

这个版本并不实用,准确的说现在没有人使用
tcp_server.cc

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

// 这样运行:./udp_server 8081
void Usage(const std::string proc)
{
    std::cout << "Usage:" << proc << " 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 error!" << errno << std::endl;
        return 2;
    }

    // 2.bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local)); // 全部清0
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1])); // 主机序列转为网络字节序
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0) // 注意类型强转
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    // 3.listen
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error!!!" << std::endl;
        return 4;
    }

    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 new link..."<<std::endl;
        // 提供服务,我们是一个死循环,意思是只能给一个用户提供服务
        while (true)
        {
            char buffer[1024];
            memset(&buffer, 0, sizeof(buffer));
            ssize_t s = read(new_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; // 返回给客户端一定的信息
                write(new_sock, echo_string.c_str(), echo_string.size());
            }
            else if (s == 0) //当对端连接断开的时候s==0
            {
                std::cout << "client quit!!!" << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error!!!" << std::endl;
                break;
            }
        }
    }
    return 0;
}

tcp_client.cc(后面几个版本的客户端都是这个,没有改变!)

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
// 我们想要这样运行: ./tcp_client server_ip server_port
void Usage(const std::string proc)
{
    std::cout << "Usage: " << proc << "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 = (uint16_t)(atoi(argv[2]));

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

    // 2.发起connect
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(svr_port);
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        std::cerr << "connect error!!!" << std::endl;
        return 3;
    }
    std::cout<<"connect success!!!"<<std::endl;

    // 这里可以进行正常的业务逻辑了
    while (true)
    {
        std::cout << "Please Enter# ";
        char buffer[1024];
        fgets(buffer, sizeof(buffer) - 1, stdin); // 从键盘上面获取信息,存入buffer中,然后通过write将buffer中的信息,\
        写入到对端去,然后再read回显一下buffer中的信息

        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;
        }
    }
    return 0;
}

版本2:多进程版本

tcp_server.cc

void Usage(const std::string proc)
{
    std::cout << "Usage:" << proc << " port" << std::endl;
}
void ServiceIO(int new_sock)
{
    //提供服务,我们是一个死循环
    while (true)
    {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = read(new_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;

            write(new_sock, echo_string.c_str(), echo_string.size());
        }
        else if (s == 0)
        {
            std::cout << "client quit ..." << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}
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 error!" << errno << std::endl;
        return 2;
    }

    // 2.bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local)); // 全部清0
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1])); // 主机序列转为网络字节序
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0) // 注意类型强转
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    // 3.listen
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error!!!" << std::endl;
        return 4;
    }

    // 在Linux中父进程忽略子进程的SIGCHID信号,子进程会自动退出,且释放资源
    signal(SIGCHLD, SIG_IGN);
    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;
        }

        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);
        std::cout << "get a new link->:[" << cli_ip << ":" << cli_port << "]#" << new_sock << std::endl;

        // 每当获取一个新的链接,我们就fork创建一个子进程,用于进行服务
        pid_t id = fork();
        if (id < 0)
        {
            continue;
        }
        else if (id == 0)
        {
            // child
            close(listen_sock);
            ServiceIO(new_sock);
            close(new_sock);
            exit(0);
        }
        else
        {
            // father
            close(new_sock);
        }
    }
    return 0;
}

曾经被父进程打开的fd,是否会被子进程继承呢?

无论父子进程中的哪一个,强烈建议关闭掉不需要的fd,如果不关闭文件描述符,会造成文件描述符资源的泄露。

运行结果:
网络编程套接字_第6张图片

版本2.1:在版本2的基础上加深

tcp_server.cc

void Usage(const std::string proc)
{
    std::cout << "Usage:" << proc << " port" << std::endl;
}
void ServiceIO(int new_sock)
{
    //提供服务,我们是一个死循环
    while (true)
    {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = read(new_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;

            write(new_sock, echo_string.c_str(), echo_string.size());
        }
        else if (s == 0)
        {
            std::cout << "client quit ..." << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}
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 error!" << errno << std::endl;
        return 2;
    }

    // 2.bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local)); // 全部清0
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1])); // 主机序列转为网络字节序
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0) // 注意类型强转
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    // 3.listen
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error!!!" << std::endl;
        return 4;
    }

    // 在Linux中父进程忽略子进程的SIGCHID信号,子进程会自动退出,且释放资源
    // signal(SIGCHLD, SIG_IGN);

    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;
        }

        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);
        std::cout << "get a new link->:[" << cli_ip << ":" << cli_port << "]#" << new_sock << std::endl;

        // 每当获取一个新的链接,我们就fork创建一个子进程,用于进行服务
        pid_t id = fork();
        if (id < 0)
        {
            continue;
        }
        else if (id == 0)
        {
            // child
            close(listen_sock);

            if(fork()>0)
            exit(0);// 退出的是子进程

            // 向后走的是孙子进程
            ServiceIO(new_sock);
            close(new_sock);
            exit(0);
        }
        else
        {
            // father
            waitpid(id,nullptr,0);//这里的等待不会阻塞
            close(new_sock);
        }
    }
    return 0;
}

版本3:线程库版

tcp_server.cc

void Usage(const std::string proc)
{
    std::cout << "Usage:" << proc << " port" << std::endl;
}
void ServiceIO(int new_sock)
{
    //提供服务,我们是一个死循环
    while (true)
    {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = read(new_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;

            write(new_sock, echo_string.c_str(), echo_string.size());
        }
        else if (s == 0)
        {
            std::cout << "client quit ..." << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}
void* HandlerRequest(void *args)
{
    pthread_detach(pthread_self());
    int sock = *(int *)args;
    delete (int *)args;

    ServiceIO(sock);
    close(sock);
}
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 error!" << errno << std::endl;
        return 2;
    }

    // 2.bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local)); // 全部清0
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1])); // 主机序列转为网络字节序
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0) // 注意类型强转
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    // 3.listen
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error!!!" << std::endl;
        return 4;
    }

    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;
        }

        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);
        std::cout << "get a new link->:[" << cli_ip << ":" << cli_port << "]#" << new_sock << std::endl;

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

曾经被主线程打开的fd,新线程是否能看到,是否共享?

——是共享的

版本4:线程池版

版本2和版本3的缺陷:
1️⃣进程线程创建无上限(资源是有限的)
2️⃣当客户链接来了,我们才给客户创建进程或者线程(效率低)

// thread_pool.hpp
#pragma once
#include 
#include 
#include 
#include  // sleep()
#include 
namespace ns_threadpool
{
    const int g_num = 5;
    template <class T>
    class ThreadPool
    {
    private:
        int num_;                  // 这个线程池有多少个线程
        std::queue<T> task_queue_; // 任务队列——这是一个临界资源
        pthread_mutex_t mtx_;
        pthread_cond_t cond_;

        static ThreadPool<T> *ins;

    private:
        ThreadPool(int num = g_num)
            : num_(num)
        {
            pthread_mutex_init(&mtx_, nullptr);
            pthread_cond_init(&cond_, nullptr);
        }

        ThreadPool(const ThreadPool<T> &tp) = delete;

        ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;

    public:
        static ThreadPool<T> *GetInstance()
        {
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

            if (ins == nullptr)// 双判定,减少锁的征用,提高效率
            {
                pthread_mutex_lock(&lock);
                // 当前单例对象还没有被创建
                if (ins == nullptr)
                {
                    ins = new ThreadPool<T>();
                    ins->InitThreadPool();
                    std::cout << "首次加载对象" << std::endl;
                }
                pthread_mutex_unlock(&lock);
            }
            return ins;
        }
        void Lock()
        {
            pthread_mutex_lock(&mtx_);
        }
        void UnLock()
        {
            pthread_mutex_unlock(&mtx_);
        }
        void Wait()
        {
            pthread_cond_wait(&cond_, &mtx_);
        }
        void WakeUp()
        {
            pthread_cond_signal(&cond_);
        }
        bool IsEmpty()
        {
            return task_queue_.empty();
        }

    public:
        // 细节:在类中要让线程执行类的成员方法,是不可行的!!!!!!! InitThreadPool回调Routine
        // 解决:必须让线程执行静态方法
        static void *Routine(void *args)
        {
            pthread_detach(pthread_self());
            ThreadPool<T> *tp = (ThreadPool<T> *)args;
            while (true)
            {
                // 由于是静态方法,所以在函数内部是无法访问类内成员的
                // 所以pthread_create中第四个参数要传递this指针
                // if (task_queue_.empty())
                // {
                //     wait();
                // }
                tp->Lock();
                // 首先检测线程池中是否有任务
                while (tp->IsEmpty()) //不用if判断,防止伪唤醒
                {
                    // 任务队列为空的话,我们需要挂起等待
                    tp->Wait();
                }
                // 当前行,队列中一定是有任务的
                T t;
                tp->PopTask(&t);
                tp->UnLock();

                // 当前线程处理任务的时候,其他线程也可能在处理任务,所以run方法写在解锁之外
                t();
            }
        }
        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < num_; i++)
            {
                pthread_create(&tid, nullptr, Routine, (void *)this);
            }
        }

        void PushTask(const T &in)
        {
            Lock();
            // 向任务队列中塞入任务
            task_queue_.push(in);
            UnLock();
            // 当任务放进去了之后,就要唤醒线程
            WakeUp();
        }

        void PopTask(T *out)
        {
            *out = task_queue_.front();
            task_queue_.pop();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&mtx_);
            pthread_cond_destroy(&cond_);
        }
    };

    // 静态成员需要在类外初始化
    template <class T>
    ThreadPool<T> *ThreadPool<T>::ins = nullptr;
}
/
// task.hpp
#pragma once
#include 
#include 
#include 
#include 
#include 
namespace ns_task
{
    class Task
    {
    private:
        int _sock;

    public:
        Task()
            : _sock(-1)
        {
        }
        Task(int sock) : _sock(sock)
        {
        }

        int Run()
        {

            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;

                write(_sock, echo_string.c_str(), echo_string.size());
            }
            else if (s == 0)
            {
                std::cout << "client quit ..." << std::endl;
            }
            else
            {
                std::cerr << "read error" << std::endl;
            }
            close(_sock);
        }

        int operator()()
        {
            return Run();
        }
        ~Task() {}
    };
} 
///
// tcp_server.cc
void Usage(const std::string proc)
{
    std::cout << "Usage:" << proc << " 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 error!" << errno << std::endl;
        return 2;
    }

    // 2.bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local)); // 全部清0
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1])); // 主机序列转为网络字节序
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0) // 注意类型强转
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    // 3.listen
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error!!!" << std::endl;
        return 4;
    }

    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;
        }

        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);
        std::cout << "get a new link->:[" << cli_ip << ":" << cli_port << "]#" << new_sock << std::endl;


        // 1.构建任务
        Task t(new_sock);
        // 2.将任务push进入线程池
        ThreadPool<Task>::GetInstance()->PushTask(t);
    }
    return 0;
}

总结

创建socket套接字的过程

  1. 调用socket(),本质是打开文件——仅仅有系统编程相关的内容。
  2. bind() , 本质是ip+port和文件信息进行关联。
  3. listen(),本质是设置该socket文件的状态处于被动状态,允许别人来连接我。
  4. accept(),获取新链接到应用层,是以fd为代表。
  5. read/write,本质就是进行网络通信,但是,对于用户来讲相当于我们在进行正常的文件读写。
  6. close(fd),关闭文件。
    a.系统层面,释放曾经申请的文件资源的结构体、连接资源等。
    b.网络层面,通知对方,我的连接已经关闭了!
  7. connect(),本质是发起链接。
    a.在系统层面,就是构建一个请求报文发送过去
    b.在网络层面,发起tcp链接的三次握手!
  8. close(), 客户端和服务端都要调用,本质在网络层面,其实就是在进行四次挥手

你可能感兴趣的:(网络,网络,服务器,linux,套接字,网络编程)