【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. 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,本质在网络层面就是在进行四次挥手

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

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风继续吹TT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值