【Linux】套接字编程

2023-10-27

目录

1. 网络初识

1.1. 协议

1.2. OSI七层模型

1.3. TCP/IP五层模型

2. socket

2.1. 源IP和目的IP

2.2. 端口号

2.3. "端口号" 和 "进程ID"

2.4. 初识TCP、UDP协议

2.5. 网络字节序

3. socket编程接口

3.1. socket 常见API

3.2. sockaddr结构

4. 基于UDP的套接字

4.1. 服务端

4.1.1. 创建套接字接口

4.1.2. 绑定

4.1.3. 提供服务

4.2. 客户端

5. 基于TCP的套接字

5.1. 服务端

5.1.1. 创建套接字

5.1.2. 绑定(bind)

5.1.3. 监听( 建立连接)(listen、accept)

5.1.4. 提供服务

5.2. 客户端

5.2.1. 创建套接字

5.2.2. 建立连接(connect)

5.2.3. 请求服务

5.3. 改进处理

5.3.1. 多进程版

5.3.2. 多线程版

5.3.3. 线程池版

6. 总结


1. 网络初识

网络在计算机中的位置。

网络也是一种软件,所以网络也是可以分层的。

1.1. 协议

协议" 是一种约定。

计算机之间的传输媒介是光信号和电信号. 通过 "频率" 和 "强弱" 来表示 0 和 1 这样的信息.。要想传递各种不同的信息, 就需要约定好双方的数据格式 。

1.2. OSI七层模型

  1. OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范;

  2. 把网络从逻辑上分为了7层. 每一层都有相关、相对应的物理设备,比如路由器,交换机;

  3. OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输;

  4. 它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯;

  5. 但是, 它既复杂又不实用; 所以我们按照TCP/IP四层模型来学习

1.3. TCP/IP五层模型

TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。 TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求 。

  • 物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等. 集线器(Hub)工作在物理层。

  • 数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作. 有以太网、令牌环网, 无线LAN等标准. 交换机(Switch)工作在数据链路层。

  • 网络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层。

  • 传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机。

  • 应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等. 我们的网络编程主要就是针对应用层。

2. socket

2.1. 源IP和目的IP

对于一个报文来讲,即表示从哪里来,到哪里去。

最大的意义:指导一个报文该如何进行路径选择。

2.2. 端口号

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

  • 端口号是一个2字节16位的整数;

  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;

  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;

  • 一个端口号只能被一个进程占用 。

2.3. "端口号" 和 "进程ID"

我们知道pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?

一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定 。

IP地址(公网IP)唯一的标识互联网中的唯一一台主机,端口号唯一的标识主机上的唯一一个进程;即 IP+port端口号 = 互联网中的唯一的一个进程。

要先通信,本质为:

  1. 先找到目标主机。

  2. 再找到该主机上的服务(进程)。

所以在互联网世界中,其实就是一个进程间通信。

2.4. 初识TCP、UDP协议

具体协议内容后面详细解释,现在先大概了解即可。

tcp协议:

  • 传输层协议

  • 有连接

  • 可靠传输

  • 面向字节流

udp协议:

  • 传输层协议

  • 无连接

  • 不可靠传输

  • 面向数据报

tcp虽然是可靠传输,udp是不可靠传输,不代表tcp就比udp协议更好,而需要根据应用场景合理选择。

2.5. 网络字节序

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

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.

  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

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

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t htohl(uint32_t netlong);
uint16_t htons(uint16_t netshort);
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

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

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

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

3. socket编程接口

3.1. 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);

网络通信的标准方式有很多种,比如:基于ip的网络通信:AF_INET,原始套接字,域间套接字。

它们都能使用同一套接口进行通信。而sockaddr就是这样的一种通用结构。

3.2. sockaddr结构

上面所提到的所有接口中,都只能用sockaddr结构,所以当使用网络套接字(AF_INET)和域间套接字(AF_UNIX)时都必须要强转成sockaddr使用。其本质类似于多态。

4. 基于UDP的套接字

4.1. 服务端

4.1.1. 创建套接字接口

#include <sys/types.h>          
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// domain: 套接字种类 AF_INET AF_UNIX (通信服务)
// type: 套接字类型 SOCK_STREAM(tcp:流式套接) SOCK_DGRAM(udp:用户数据报)
// protocol:套接字协议类型(tcp、udp下为0)
// 返回值为文件描述符,出错返回-1

代码:

#include<iostream>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
using namespace std;

int main()
{
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock<0)
    {
        cerr << "socket create error:" << errno << endl;
        return 1;
    }
    cout << sock << endl;
    return 0;
}

4.1.2. 绑定

对于客户端来说,需要知道该服务器的ip和端口。

#include <sys/types.h>          
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
// sockfd:创建好的套接字的文件描述符
// addr:套接字协议
// addrlen:套接字大小
// 返回值:成功返回0,绑定失败返回-1.

点分十进制IP转化四字节整数IP接口:

代码:

#include<iostream>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h> //要使用sockaddr_in 需要包含这两个头文件
#include<arpa/inet.h>
using namespace std;

string Usage(string proc) // 使用手册
{
    cout << "Usage: " << proc << "port" << endl;
}

int main(int argc, char*argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return -1;
    }
    uint16_t port = atoi(argv[1]); // 命令行接收port,可以采用任意端口
    // 1.创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock<0)
    {
        cerr << "socket create error:" << errno << endl;
        return 1;
    }
    cout << sock << endl;

    // 2.给该服务器绑定ip和端口
    struct sockaddr_in local;
    local.sin_family = AF_INET; // ipv4 协议 ,协议家族使用网络套接字
    local.sin_port = htons(port); // 此处的端口号,是我们计算机上的变量,是主机序列,需要转换成网络序列
    local.sin_addr.s_addr = INADDR_ANY; // 绑定IP地址(INADDR_ANY:0)
    // a.需要将点分十进制,字符串IP地址,转化成四字节整数IP
    // b.需要考虑大小端
    // in_addr_t inet_addr(const char *cp); 能完成上面的两个工作
    // 但是云服务器上不允许用户直接bind公网IP,而且实际编写时也不会指明IP
    // 如果bind的是确定的IP(主机),意味着只有发送到该IP主机上面的数据,才会交给你的网络进程
    // 但是一般服务器可能有多张网卡,配置多个IP,我们需要的不是某个IP上面的数据,而是所有发送到该主机的数据

    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0) // 为了满足上述接口参数类型,这里需要将sockaddr_in类型强制转化成sockaddr。
    {
        cerr << "bind error : " << errno << endl;
        return 2;
    }
    
    return 0;
}

4.1.3. 提供服务

所谓提供服务,即读取客户端发送的数据,进行处理,并且返回处理结果。

udp所用的接口为:

#include <sys/types.h>
#include <sys/socket.h>

// 接收数据
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、addrlen:输入输出型参数,表示客户端socket信息(将数据处理结果返回给客户端时,需要用到)
// 返回值 ssize_t:读取信息的大小

// 返回数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);
// 参数与上面相同

udp_server整体代码为:

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


string Usage(string proc)
{
    cout << "Usage: " << proc << "port" << 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)
    {
        cerr << "socket create error:" << errno << endl;
        return 1;
    }
    cout << sock << endl;

    // 2.给该服务器绑定ip和端口
    struct sockaddr_in local;
    local.sin_family = AF_INET; // 协议家族使用网络套接字
    local.sin_port = htons(port); // 此处的端口号,是我们计算机上的变量,是主机序列,需要转换成网络序列
    // a.需要将点分十进制,字符串IP地址,转化成四字节整数IP
    // b.需要考虑大小端
    // in_addr_t inet_addr(const char *cp); 能完成上面的两个工作
    // 但是云服务器上不允许用户直接bind公网IP,而且实际编写时也不会指明IP
    // 如果bind的是确定的IP(主机),意味着只有发送到该IP主机上面的数据,才会交给你的网络进程
    // 但是一般服务器可能有多张网卡,配置多个IP,我们需要的不是某个IP上面的数据,而是所有发送到该主机的数据
    local.sin_addr.s_addr = INADDR_ANY; // (INADDR_ANY:0)

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

    // 3. 提供服务
    bool quit = false;
    #define NUM 1024
    char buffer[NUM] ;
    while (!quit) // 对于服务器来说,应该是24小时都能提供服务,所以这里是死循环
    {
        // 客户端信息
        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;
            cout << "client# " << buffer << endl;
            string echo_hello = buffer;
            echo_hello += "...";
            sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr *)&peer, len);

        }
        else
        {
            cout << "recvfrom error" << errno << endl;
            return 3;
        }
        
    }

    return 0;
}

4.2. 客户端

其中操作基本一致:

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

void Usage(string proc) // 使用手册 (使用方式为:./udp_client ip port)
{
    cout << "Usage: \n\t" << proc << " server_ip serverport" << endl;
}
int main(int argc, char*argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    // 1. 创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock<0)
    {
        cout << "socket error : " << errno << endl;
    }

    // 客户端不需要显式bind,一旦显式bind,就必须明确client要和哪一个port关联
    // client指明的端口号,可能在client端被占用,导致客户端无法使用。
    // server的port必须明确,而且不能改变,但client只要有就行,一般由OS自动bind
    // 在client正常发送数据的时候,OS自动bind,采用随机端口的方式
    
     // 数据发送给谁
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2])); 
    // 传入的第三个参数为端口号,但是类型为char,需要转化成整型;但这个整型是主机序列,再将这个整型转化成网络序列
    server.sin_addr.s_addr = inet_addr(argv[1]); 
    // 这里的目的IP不能是INADDR_ANY,需要给出具体ip,而传入的第二个参数就是ip地址,所以直接使用inet_addr转化成四字节整型即可
    
    // 2.使用服务
    while(1)
    {
        // a.数据从哪里来
        string message;
        cout << "输入# ";
        cin >> message;
        
         // 发送数据
        sendto(sock, message.c_str(), message.size(), 0, (struct 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;
            cout << "server echo#" << buffer << endl;
        }
        else
        {
            cout << "recvfrom error" << errno << endl;
            return 2;
        }
    }
    return 0;
}

5. 基于TCP的套接字

5.1. 服务端

5.1.1. 创建套接字

这部分与udp几乎一样,唯一区别就在于socket第二个参数为流式套接,就直接放代码。

int sock = socket(AF_INET, SOCK_STREAM, 0); // ipv4 , 流式套接c
if(sock<0)
{
    cerr << "socket error: " << errno << endl;
    return 2;
}

5.1.2. 绑定(bind)

这部分与udp一模一样无任何区别。

struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 将local清0
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
    cerr << "bind error" << errno << endl;
    return 3;
}

5.1.3. 监听( 建立连接)(listen、accept)

因为tcp与udp不同,udp通信前不需要与客户端建立连接,而tcp是需要连接的。这里的服务器端是被动接受客户端的连接。

因为服务器需要在任何时候都能为客户端提供服务,所以这一过程应该是周而复始的等待客户的到来,所以这一过程被称为监听

监听接口:

#include <sys/types.h>          
#include <sys/socket.h>

int listen(int sockfd, int backlog);  // 将套接字设为监听状态

// sockfd: 监听套接字
// backlog: 后面解释, 现在先设置为5
// 返回值: 成功返回0, 失败返回-1

接受连接接口:

#include <sys/types.h>          
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr,socklen_t*addrlen); // 接受客户端发起的连接
// sockfd: 监听套接字,用于监听客户端发起的连接 
// addr: 监听套接字协议
// addrlen: 监听套接字大小
// 返回值: 成功返回非0整数,是一个文件描述符,是提供服务的套接字

代码:

// 监听
const int back_log = 5;
if (listen(listen_sock, back_log) < 0)
{
    cerr << "listen error" << errno << endl;
    return 4;
}
while(true) // 一直循环接受客户端发起的连接
{
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len); 
    // 上面创建的监听套接字只起到监听作用
    // accept接口返回的套接字,才能为客户端提供服务
    if(new_sock < 0) // 如果未收到套接字,则继续循环接收
    {
        continue;
    }
 }

5.1.4. 提供服务

tcp协议中的数据是以字节流的方式传输,所以服务器可以使用read、write; recv、send等方式接收数据。

这里我们使用read、write方式接收。

整体代码为:

#include<iostream>
#include<string>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
using namespace std;

void Usage(string proc)
{
    cout << "Usage: " << proc << "port" << 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); // ipv4 , 流式套接
    if(listen_sock < 0)
    {
        cerr << "socket error: " << errno << endl;
        return 2;
    }
    // bind
    // 填充套接字信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    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)
    {
        cerr << "bind error" << errno << endl;
        return 3;
    }
    // 监听
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        cerr << "listen error" << errno << endl;
        return 4;
    }
    while(true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len); 
        // 上面创建的监听套接字只起到监听作用
        // accept接口返回的套接字,才能为客户端提供服务
        if(new_sock < 0)
        {
            continue;
        }
	    cout<<"get a new link..."<< endl;
        // 提供服务
        while(true) //同样也是循环读取数据
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t cnt = read(new_sock, buffer, sizeof(buffer) - 1); 
            // 从new_sock套接字中读取数据,存放在buffer中
            if(cnt > 0)
            {
                buffer[cnt] = 0;
                // 在数据末尾添加'\0'
                cout << "client# " << buffer << endl;
                string echo_string = "server--> ";
                echo_string += buffer;
                write(new_sock, echo_string.c_str(), echo_string.size());
                //将数据写回给客户端
            }
            else if(cnt == 0) //如果读到的个数为0,则表示客户端已经停止发送数据,则退出服务
            {
                cout << "client quit..." << endl;
                break;
            }
            else //如果读取的个数小于0,则表示读取失败,退出服务
            {
                cerr << "read error" << endl;
                break;
            }
        }
    }
    return 0;
}

5.2. 客户端

5.2.1. 创建套接字

代码与服务器一致,直接贴代码:

#include<iostream>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<cerrno>
using namespace std;

// ./udp_client server_ip server_port
void Usage(string proc) // 使用手册
{
    cout << "Usage: " << proc << "server_ip server_port" << endl;
}
int main(int argc, char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        return 1;
    }
    string server_ip = argv[1]; // 保存输入的服务器ip
    uint16_t server_port = atoi(argv[2]); // 保存输入的服务器port

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

5.2.2. 建立连接(connect)

同udp一样,客户端不需要显式bind,OS会自动bind,因为显式bind可能会导致端口被占用,导致客户端bind失败。

connect接口:

#include <sys/types.h>         
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); // 向服务器发送连接
// sockfd: 客户端套接字
// addr: 服务器地址
// addrlen: 服务器地址长度
// 返回值: connect成功返回0,否则返回-1

代码为:

// 2.connect
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port); // 主机序列转化成网络序列
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 点分十进制ip转化成四字节ip,再将主机序列转化成网络序列
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) // 返回值小于0,连接失败
{
    cout << "connect server failed !" << endl;
    return 3;
}

5.2.3. 请求服务

业务请求的代码如下,即客户端一直从键盘获取数据,然后发送给服务器,最后接收从服务器返回的数据,并输出到屏幕。

// 3. 业务请求
while(true)
{
        cout << "Please Enter# ";
        char buffer[1024];
        fgets(buffer, sizeof(buffer), stdin); // 从键盘上获取内容
        write(sock, buffer, strlen(buffer)); // 将获取到的数据写进sock
        ssize_t cnt = read(sock, buffer, sizeof(buffer) - 1); // 读取服务器返回的内容
        if(cnt > 0)
        {
            buffer[cnt] = 0;
            cout << "server echo# " << buffer << endl;
        }
 }

5.3. 改进处理

通过上面的tcp_server、tcp_client的代码执行之后会发现一个问题,就是这个服务器一次只能允许一个客户端连接。

实际中不会采用这种方式。

所以下面会对服务器进行部分改进。

5.3.1. 多进程版

通过fork出子进程,让子进程去执行提供服务部分,从而父进程可以继续接收客户端发起的新连接.

#include<iostream>
#include<string>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<signal.h>
using namespace std;

void Usage(string proc)
{
    cout << "Usage: " << proc << "port" << endl;
}
void ServiceIO(int new_sock)
{
    // 提供服务
        while(true)
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t cnt = read(new_sock, buffer, sizeof(buffer) - 1);
            if(cnt > 0)
            {
                buffer[cnt] = 0;
                cout << "client# " << buffer << endl;
                string echo_string = "server--> ";
                echo_string += buffer;
                write(new_sock, echo_string.c_str(), echo_string.size());
            }
            else if(cnt == 0)
            {
                cout << "client quit..." << endl;
                break;
            }
            else
            {
                cerr << "read error" << 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); // ipv4 , 流式套接
    if(listen_sock < 0)
    {
        cerr << "socket error: " << errno << endl;
        return 2;
    }
    // bind
    // 填充套接字信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    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)
    {
        cerr << "bind error" << errno << endl;
        return 3;
    }
    // 监听
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        cerr << "listen error" << errno << endl;
        return 4;
    }

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

    while(true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len); 
        // 上面创建的监听套接字只起到监听作用
        // accept接口返回的套接字,才能为客户端提供服务
        if(new_sock < 0)
        {
            continue;
        }
        // peer: 输入输出型参数,存放客户端sock
        // 拿到客户端ip地址及端口:
        uint16_t client_port = ntohs(peer.sin_port); // 网络序列转主机序列
        string client_ip = inet_ntoa(peer.sin_addr); // inet_ntoa:将四字节ip转化成点分十进制ip
        cout << "get a new link -> : [" << client_ip << ":" << client_port << "]#" << new_sock << endl;
        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
            // do nothing
            close(new_sock); // 父进程关闭提供服务的文件描述符,继续接收新链接
        }
        }
    return 0;
}

5.3.2. 多线程版

采用多线程方式,创建新线程去提供服务,主线程继续接收客户端发起的链接。

#include<iostream>
#include<string>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<signal.h>
#include<pthread.h>
using namespace std;

void Usage(string proc)
{
    cout << "Usage: " << proc << "port" << endl;
}
void ServiceIO(int new_sock)
{
    // 提供服务
        while(true)
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t cnt = read(new_sock, buffer, sizeof(buffer) - 1);
            if(cnt > 0)
            {
                buffer[cnt] = 0;
                cout << "client# " << buffer << endl;
                string echo_string = "server--> ";
                echo_string += buffer;
                write(new_sock, echo_string.c_str(), echo_string.size());
            }
            else if(cnt == 0)
            {
                cout << "client quit..." << endl;
                break;
            }
            else
            {
                cerr << "read error" << endl;
                break;
            }
        }
}

void* HandlerRequest(void*args)
{
    pthread_detach(pthread_self()); // 分离新线程
    int sock = *(int *)args;
    delete (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); // ipv4 , 流式套接
    if(listen_sock < 0)
    {
        cerr << "socket error: " << errno << endl;
        return 2;
    }
    // bind
    // 填充套接字信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    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)
    {
        cerr << "bind error" << errno << endl;
        return 3;
    }
    // 监听
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        cerr << "listen error" << errno << endl;
        return 4;
    }

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

    while(true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len); 
        // 上面创建的监听套接字只起到监听作用
        // accept接口返回的套接字,才能为客户端提供服务
        if(new_sock < 0)
        {
            continue;
        }
        // peer: 输入输出型参数,存放客户端sock
        // 拿到客户端ip地址及端口:
        uint16_t client_port = ntohs(peer.sin_port); // 网络序列转主机序列
        string client_ip = inet_ntoa(peer.sin_addr); // inet_ntoa:将四字节ip转化成点分十进制ip
        cout << "get a new link -> : [" << client_ip << ":" << client_port << "]#" << new_sock << endl;
        
        // 创建新线程
        pthread_t tid;
        int *pram = new int(new_sock);
        pthread_create(&tid, nullptr, HandlerRequest, pram);
        // 这里主线程不需要关闭new_sock文件描述符,因为主线程与新线程共享一个文件描述符数组,主线程关闭new_sock会导致新线程也关闭
        }
    return 0;
}

5.3.3. 线程池版

上面的两种方法虽有改进,但是如果客户端请求连接数量过大,会导致服务器频繁的创建进程、线程,开销过大;并且安全性较低,如果被服务器攻击很容易就会崩溃。

所以使用线程池优于上面的两种版本。

tcp_server.cpp:

#include<iostream>
#include<string>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<signal.h>
#include<pthread.h>
#include"thread_pool.hpp"
#include"task.hpp"
using namespace std;

void Usage(string proc)
{
    cout << "Usage: " << proc << "port" << 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); // ipv4 , 流式套接
    if(listen_sock < 0)
    {
        cerr << "socket error: " << errno << endl;
        return 2;
    }
    // bind
    // 填充套接字信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    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)
    {
        cerr << "bind error" << errno << endl;
        return 3;
    }
    // 监听
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        cerr << "listen error" << errno << endl;
        return 4;
    }

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

    while(true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len); 
        // 上面创建的监听套接字只起到监听作用
        // accept接口返回的套接字,才能为客户端提供服务
        if(new_sock < 0)
        {
            continue;
        }
        // peer: 输入输出型参数,存放客户端sock
        // 拿到客户端ip地址及端口:
        uint16_t client_port = ntohs(peer.sin_port); // 网络序列转主机序列
        string client_ip = inet_ntoa(peer.sin_addr); // inet_ntoa:将四字节ip转化成点分十进制ip
        cout << "get a new link -> : [" << client_ip << ":" << client_port << "]#" << new_sock << endl;
        // 1.创建任务
        Task t(new_sock);
        // 2.将任务放进线程池
        ThreadPool<Task>::GetInstance()->PushTask(t);
        }
    return 0;
}

thread_pool.hpp:

#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
using namespace std;

template <class T>
class ThreadPool
{
private:
    int _capacity;
    queue<T> _task_queue; // 临界资源

    pthread_mutex_t _mtx;
    pthread_cond_t _cond;

    static ThreadPool<T> *ins;

private:
    // 构造函数必须得实现,但是必须私有化(即不能实例化对象)
    ThreadPool(int capacity = 5) : _capacity(capacity)
    {
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool<T> &tp) = delete;
    // 赋值语句
    ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;

public:
    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:
    static ThreadPool<T> *GetInstance() // 设置为静态函数该函数属于类,否则在main函数中,如果未实例化对象将不能调用该函数
    {
        static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
        if (ins == nullptr) // 双判定,只有未创建单例对象时才竞争锁,减少锁的争用,提高获取单例的效率
        {
            pthread_mutex_lock(&lock);
            if (ins == nullptr) // 当前单例对象没有被创建
            {
                ins = new ThreadPool<T>;
                ins->InitThreadPool();
            }
            pthread_mutex_unlock(&lock);
        }
        return ins;
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mtx);
        pthread_cond_destroy(&_cond);
    }

    // 在类中要让线程执行类内成员方法是不可行的
    // 由于类中非静态成员隐含this指针,这里会导致传参出错,所以需要将该函数设置为静态
    static void *Rountine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadPool<T> *tp = (ThreadPool<T> *)args;
        while (true)
        {
            tp->Lock();
            while (tp->IsEmpty()) // 任务队列为空
            {
                tp->Wait(); // 挂起
            }
            T t;
            tp->PopTask(&t);
            tp->Unlock();

            t(); // 处理任务
            // sleep(1);
        }
    }
    void InitThreadPool()
    {
        pthread_t tid;
        for (int i = 0; i < _capacity; ++i)
        {
            pthread_create(&tid, nullptr, Rountine, (void *)this); // 由于该执行任务函数为静态函数,不能访问类内成员,所以传参数时,需要传入this指针才能访问到类内成员
        }
    }

    void PushTask(const T &in)
    {
        Lock();
        _task_queue.push(in);
        Unlock();
        Wakeup();
    }
    void PopTask(T *out)
    {
        *out = _task_queue.front();
        _task_queue.pop();
    }
};

// 初始化类内静态成员
template <class T>
ThreadPool<T> *ThreadPool<T>::ins = nullptr;

task.hpp:

#pragma once

#include <iostream>
#include <pthread.h>
#include<cstring>
#include<unistd.h>
using namespace std;

class Task
{
private:
    int sock;

public:
    Task() :sock(-1){}
    Task(int _sock) : sock(_sock)
    {}
    int run()
    {
    // 提供服务
        while(true)
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t cnt = read(sock, buffer, sizeof(buffer) - 1);
            if(cnt > 0)
            {
                buffer[cnt] = 0;
                cout << "client# " << buffer << endl;
                string echo_string = "server--> ";
                echo_string += buffer;
                write(sock, echo_string.c_str(), echo_string.size());
            }
            else if(cnt == 0)
            {
                cout << "client quit..." << endl;
                //break;
            }
            else
            {
                cerr << "read error" << endl;
                //break;
            }
        }
        close(sock);
    }

        int operator()()
    {
        return run();
    }
}; 

6. 总结

  1. 创建socket的过程(socket()),本质是打开文件。(仅有系统相关的内容)
  2. 2.bind(),struct sockaddr_in -> ip,port,本质是ip+port和文件信息进行关联
  3. listen(),本质是设置该socket文件的状态,允许别人来连接我
  4. accpet(),获取新链接到应用层,是以fd为代表的;所谓的连接,在OS层面,本质其实就是一个描述连接的结构体(文件)
  5. read/write,本质就是进行网络通信,对于用户来讲就相当于在进行正常的文件读写
  6. close(fd),关闭文件;系统层面,释放曾经申请的文件资源,连接资源等;网络层面,通知对方,我的连接已经关闭了
  7. connect(),本质是发起连接,在系统层面,就是构建一个请求报文发送过去;网络层面,发起tcp连接的三次握手
  8. close(),client、server,本质在网络层面就是在进行四次挥手

(三次握手、四次挥手后面会详细说)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Linux】套接字编程 的相关文章

随机推荐

  • 基于FPGA的正弦波发生器设计与实现

    基于FPGA的正弦波发生器设计与实现 摘要 本文介绍了一种基于FPGA的正弦波发生器的设计与实现 通过使用FPGA的数字信号处理功能 可以实现高精度 高性能的正弦波生成 文章首先介绍了DDS Direct Digital Synthesis
  • 大骗局星钻共享拍卖不为人知的的秘密

    钻石恒久远 一颗永流传 作为当之无愧的宝石之王 钻石从开采到初步打磨再到深层加工最后到售卖 需要经历无数道工序流程 平均每开采一克拉的钻石胚 需要至少处理250吨的矿石 而这一克拉的钻石胚还需要经过切磨雕琢 最后以闪耀动人的钻石成品面世时
  • vue自定义校验规则的动态必填字段

  • 10秒学会codeblocks里批量替换变量名

    10秒学会codeblocks里批量替换变量名 我想把下面代码所有的frontt改成front 应该怎么做呢 typedef struct QueueElementType element MAXSIZE int frontt int re
  • Latex基本使用

    一 文字 加粗 textbf 文字 加颜色 textcolor 颜色 文字 如 textcolor cyan TABLE II 一个单词的首字母下沉占用两行 单词剩余部分大写 IEEEPARstart 单词首字母 单词剩余部分 如 IEEE
  • Linux下的FILE*结构体

    FILE 结构体解析 struct file结构体定义在include Linux fs h中定义 文件结构体代表一个打开的文件 系统中的每个打开的文件在内核空间都有一个关联的 struct file 它由内核在打开文件时创建 并传递给在文
  • 笔记本网络计算机和设备不可见,xp电脑不显示无线网络的七种原因和解决方法...

    xp纯净版系统电脑打开后发现桌面右下角不显示无线网络 如果要设置无线网络都不知道从哪里下手 这到底是怎么回事 造成xp系统不显示无线网络的原因有很多种 下面和大家讲解一下xp电脑不显示无线网络的七种原因和解决方法 故障原因 一 无线网卡驱动
  • k8s config多集群管理

    k8s config多集群管理 contexts 查看 kubectl config get contexts 创建 kubectl config set context my context 修改 kubectl config set c
  • pycharm整体缩进,整体取消缩进

    整体缩进 tab 整体取消缩进 tab shift
  • [游戏商业化]一些基础概念点,做个记录

    A 商业化业务逻辑 核心 三者之间的关系 产品的最终目标是实现盈利 获取利润 产品的主要目标是发展用户 吸引用户 留下用户 通过投放产品广告为产品带来用户 变现 广告变现 是最简单 最有效且不存在领域限制的变现方式 通过在App上展示广告主
  • 数据结构--- 树

    一 知识补充 定义 树是一种数据结构 它是由n n 0 个有限节点组成一个具有层次关系的集合 把它叫做 树 是因为它看起来像一棵倒挂的树 也就是说它是根朝上 而叶朝下的 它具有以下的特点 每个节点有零个或多个子节点 没有父节点的节点称为根节
  • 独家专访BlockCity区块城市徐志翔:DAO是未来元宇宙的核心

    转载说明 最近随着ChatGPT的出圈 整个AIGC领域倍受关注 唱衰媒体人的声音也开始不绝于耳 但看到这样有质量的长文 我想也不是每个媒体人都将会被AI替代吧 本文来自前瞻 元宇宙观察 专栏记者采访 作者声明分享无版权限制 如下为全文内容
  • 基础算法题 —— 合唱队(最长递增子序列)

    题解 枚举每个位置左右侧分别所能站的做多人 自左向右递增 求每个位置左边最多可站多少人 含自己 dp1 自右向左递增 求每个位置右边最多可站多少人 含自己 dp2 选择第 i 个位置不移动的情况下 合唱队所能站的人数 dp1 i dp2 i
  • 学习文件day20--关于File

    java io File File的每一个实例用于表示硬盘上的一个文件或目录 实际上表示的是一个抽象路径 File可以 1 访问其表示的文件或目录的属性信息 名字 大小 修改时间等 2 操作文件或目录 创建 删除 3 访问一个目录中的所有子
  • Flutter之瀑布流效果——Flutter基础系列

    需求 相信android和ios的瀑布流效果大家都试过 网上有很多实现方法和开源库 今天我来为大家介绍一下如何在Flutter中实现瀑布流 整理一下方便以后学习 顺便分享给大家 一 生成二维码 1 导入依赖 在 pubspec yaml 中
  • R手册(Machine Learning)--mlr (Part 2)

    文章目录 Configuration 配置 Parallelization 并行 Imputation 插补 Feature Extraction 特征提取 1 Feature filtering 特征筛选 2 Feature select
  • 排序算法总结—时间复杂度O(n)—基数排序/计数排序小记

    排序算法总结 时间复杂度O n 基数排序 基数排序 分为最高位优先和最低位优先的算法 找到最大值max 求出max的位数 在max位数max length进行循环max length趟 对于每一位进行排序 对于一个数字要会从低位一位一位取值
  • Flutter导航返回拦截(WillPopScope)

    onWillPop是一个回调函数 当用户点击返回按钮时被调用 包括导航返回按钮及Android物理返回按钮 该回调需要返回一个Future对象 如果返回的Future最终值为false时 则当前路由不出栈 不会返回 最终值为true时 当前
  • 基于LSTM的股票时间序列预测(附数据集和代码)

    LSTM 数据集 实战 如果对LSTM原理不懂得小伙伴可以看博主下一篇博客 因为博主水平有限 结合其他文章尽量把原理写的清楚些 数据集 首先附上数据集 链接 https pan baidu com s 1AKsz ohmYHr9mBEEh7
  • 【Linux】套接字编程

    目录 1 网络初识 1 1 协议 1 2 OSI七层模型 1 3 TCP IP五层模型 2 socket 2 1 源IP和目的IP 2 2 端口号 2 3 端口号 和 进程ID 2 4 初识TCP UDP协议 2 5 网络字节序 3 soc