【网络通信】socket编程——TCP套接字

news2025/1/4 16:13:29

TCP依旧使用代码来熟悉对应的套接字,很多接口都是在udp中使用过的
所以就不会单独把他们拿出来作为标题了,只会把第一次出现的接口作为标题

文章目录

  • 服务端 tcp_server
    • tcpserver.hpp(封装)
      • 初始化 initServer
        • 1. 创建socket
        • 2. 绑定 bind
          • htons —— 主机序列转化为网络序列
        • 3.监听
          • listen ——设为 监听状态
      • 启动 Start
        • 1.获取连接,accept
          • accept
          • accept返回的文件描述符 与 socket设置成功返回的文件描述符的关系
        • 2.获取新连接成功,开始进行业务处理
    • tcpserver.cc (主函数main实现)
  • 客户端 tcp_client
    • tcpclient.cc(不封装,直接实现)
      • 1.创建套接字
      • 2.发起链接
        • inet_addr——字符串IP地址 转为 网络序列IP地址
      • 3. 链接成功
  • 具体代码实现
    • err.hpp(用于存放错误信息)
    • makefile
    • tcpServer.hpp( 服务端 封装)
    • tcpServer.cc( 服务端 主函数实现)
    • tcpClient.cc(客户端 不封装)

通过TCP的套接字 ,来把数据交付给对方的应用层,完成双方进程的通信

服务端 tcp_server

tcpserver.hpp(封装)

在 tcpServer.hpp 中,创建一个命名空间 yzq 用于封装
在命名空间中,定义一个类 TcpServer

该类中包含 构造 析构 初始化(initServer) 启动(start)


初始化 initServer

1. 创建socket

设置监听端口号(后面会解释) ,需要端口号标识进程的唯一性


在类外设置一个默认端口号8888作为构造函数参数port的缺省值


创建套接字


输入 man socket

第一个参数 domain ,用于区分 进行网络通信还是 本地通信
若想为网络通信,则使用 AF_INET
若想为本地通信,则使用 AF_UNIX

第二个参数 type, 套接字对应的服务类型


SOCK_STREAM 流式套接
SOCK_DGRAM 无连接不可靠的通信(用户数据报)

第三个参数 protocol ,表示想用那种协议,协议默认为0
若为 流式套接,则系统会认为是TCP协议 ,若为用户数据报,则系统会认为是UDP协议

套接字的返回值:若成功则返回文件描述符,若失败则返回 -1


说明进行网络通信,流式套接,同时系统认为是TCP协议


创建err.hpp 用于存储错误信息的枚举


如果创建失败,则终止程序

2. 绑定 bind

输入 man 2 bind ,查看绑定

给一个套接字绑定一个名字
第一个参数 sockfd 为 套接字
第二个参数 addr 为 通用结构体类型
第三个参数 addrlen 为 第二个参数的实际长度大小

bind返回值:若成功,则返回0,若失败,返回 -1


使用bind,是需要借助一个通用结构体来实现的
所以定义一个 网络通信类型的结构体 local

在上一篇博客中,详细讲述了 sockaddr_in 结构体的内部组成
不懂可以去看看:struct sockaddr_in 的理解


htons —— 主机序列转化为网络序列

输入 man htons ,表示短整数的主机转网络序列

所以需要将主机的port_进行转化 ,然后再交给 local的sin_port (端口号)


INADDR_ANY 表示bind的任意IP


如果绑定失败返回-1


3.监听

listen ——设为 监听状态

输入 man 2 listen
设置当前套接字状态为 监听状态

第一个参数 sockfd 为 套接字
第二个参数 暂不做解释,一般设为整数
若成功则返回0,若失败返回-1


监听失败 返回-1,并终止程序


在类外设置一个 默认整数 为32

启动 Start

设置一个布尔变量 quit_,若为true则表示 服务器启动, 若为false,则表示 服务器没有启动


如果服务器没有启动,则进入while循环

1.获取连接,accept

accept

输入 man 2 accept

需要知道谁连的你,所以要获取到客户端的相关信息

第一个参数 sockfd 为套接字
第二个参数 addr 为通用结构体类型的 结构体 这个结构体是用来记录客户端内的port号以及IP地址 、16位地址类型等信息
第三个参数 addrlen 为 结构体的大小

返回值:
若成功,则返回一个合法的整数 即文件描述符
若失败,返回-1并且设置错误码

accept返回的文件描述符 与 socket设置成功返回的文件描述符的关系

如:有一个鱼庄,生意不太好,所以在外面站着一个人叫张三,进行揽客
有一天你和你的朋友在外面遇见张三,张三就向你们说他们鱼庄有多少,推荐去他们哪里吃鱼
正好你们俩也饿了,所以就跟张三去鱼庄吃鱼,但是只有你们进入鱼庄了,张三并没有进去
张三只是向里面喊了一声,来客人了,然后继续找人去了
这个时候来了一个服务员李四,向你们询问要吃什么,并向你们提供各种服务

每一次张三把客人招呼到鱼庄时,都会有一名服务员给客人提供服务
当张三做完自己的工作后,立马返回自己的工作岗位,继续招揽客人

张三不给用户提供具体的服务,只负责把客人从路上拉到店里去吃饭 进行消费
李四来给客人提供服务
鱼庄 可以看作是 整个服务器
像张三这样把客人从外部 拉到餐厅里的 称为 监听套接字 即accept的第一个参数 sockfd
像李四这样作的动作,相当于accept会返回一个文件描述符,这个文件描述符 是真正给用户提供IO服务的


若张三继续拉客,在路上碰见一个人,问他要不要去鱼庄吃饭,但那个人摇了摇头,表示没有意愿去鱼庄吃饭,
此时张三就被拒绝了,但这并不影响张三继续拉客去鱼庄
所以 accept 获取失败,只需继续 执行即可

2.获取新连接成功,开始进行业务处理

提供一个service的函数 ,参数为新的文件描述符sock
用于实现基本的读写服务 即 客户端发消息,需要把消息转回去


TCP 是一种流式服务
输入 man 2 read

从文件描述符fd中将我们想要的数据,按照数据块的方式读取出来
返回值代表多少字节,读取到文件结尾为0,失败为-1


将sock中的数据读取到buffer缓冲区中
若读取成功,则将最后一位的下一位赋值为0


若read的返回值为0,则对方将连接关闭了,所以sock也可以关闭


若返回值小于0,则读取失败,返回错误码


收到消息,需要把消息做某种处理后,再把消息转回去
所以使用 包装器 functional处理

在类外设置一个函数类型,返回值为string,参数为 string 的包装器


用该函数类型定义为一个私有变量func



将处理完的消息进行返回
输入 man 2 write
向一个文件中写入信息

fd代表文件描述符
buf代表 缓冲区
count代表 缓冲区大小
write将缓冲区的count大小的数据写入 fd中


将res中的数据 写入 sock文件描述符中

tcpserver.cc (主函数main实现)

想要只输入 ./tcp_server 加 端口号
所以在main函数中添加命令行参数
main函数的两个参数,char* argv[] 为指针数组 ,argv为一张表,包含一个个指针,指针指向字符串
int argc,argc为数组的元素个数


当参数输入不为2时,就会终止程序,同时打印出对应的输入参数


通过构造函数了解, 想要使用 new TcpServer 需要传入回调和端口号


客户端 tcp_client

tcpclient.cc(不封装,直接实现)

为了使用客户端,所以要输入对应的 可执行程序 serverip serverport
所以在main函数需要使用 命令行参数


若输入的参数少于3个,则终止程序,并打印出对应输入的参数


将输入的第二个参数的IP地址 赋值给 serverip
将输入的第三个参数的端口号,使用atoi将字符串转化为整数 ,再赋值给serverport

1.创建套接字

网络通信,并为流式套接,默认为0,因为流式所以为TCP协议
若创建套接字失败,则终止程序


2.发起链接

输入 man accept

客户端 通过套接字sockfd,向特定的服务器发起链接请求
sockfd:套接字
addr:公共类型的结构体 内部包含 服务器的IP地址和的端口号
addrlen:结构体的大小

返回值:若成功,则返回0,若失败,返回-1和错误码
首次发起链接时,操作系统会给客户端自动进行绑定端口


所以需要先定义一个结构体server

借助htons 将上述的主机序列端口号serverport 转化为网络序列端口号

inet_addr——字符串IP地址 转为 网络序列IP地址

输入man inet_addr

第一个参数为 字符串风格的IP地址
第二个参数 为 网络序列的IP地址
将 字符串风格的IP地址 转为 网络序列的IP地址


再将主机序列的IP地址serverip,转化为网络序列的IP地址


cnt表示重连次数
设置while循环,当不等于0链接失败时,cnt值减1,并重新链接,若cnt值为0,则break终止循环
若出了while循环,cont小于等于0,则终止程序


3. 链接成功

创建一个string类型的line,将输入的参数传入line中
使用write,将line的内容传入文件描述符中
使用read,将sock的数据传入buffer中
通过read的返回值来判断,若返回值大于0则,输出其中内容
若返回值等于0,则说明链接关闭,则退出while循环
若返回值小于,则说明创建失败,返回错误码

具体代码实现

err.hpp(用于存放错误信息)

#pragma once 

enum
{
  USAGE_ERR=1,
  SOCKET_ERR,//2
  BIND_ERR,//3
  LISTEN_ERR//4
};


makefile

.PHONY:all
all: tcp_client tcp_server

tcp_client:tcpClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread
tcp_server:tcpServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f tcp_client tcp_server


tcpServer.hpp( 服务端 封装)

#pragma once

#include<iostream>
#include<cstdlib>
#include<string.h>
#include<unistd.h>
#include"err.hpp"
#include <sys/types.h>          
#include <sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<functional>



namespace yzq
{
    
    static uint16_t defaultport=8888;//默认端口号
    static const int backlog=32;//默认整数为32
    using func_t=std::function<std::string(const std::string&)>;

    class TcpServer;
    class ThreadData//该类用于存放客户端的IP port 套接字
    { 
        public:
        ThreadData(int fd,const std::string&ip,const uint16_t &port,TcpServer*ts)//构造
        :sock(fd),clientip(ip),clientport(port),current(ts)
        {}
      public:
       int sock;//套接字
       std::string clientip;//客户端IP
       uint16_t clientport;//客户端端口号
       TcpServer*current;
    };
    
   class TcpServer
   {
    public:
    TcpServer(func_t func,uint16_t port=defaultport)
    :func_(func),port_(port),quit_(true)//表示默认启动
    {}
    void initServer()//初始化
    {
        //1.创建socket
        listensock_=socket(AF_INET,SOCK_STREAM,0);
        if(listensock_<0)//创建失败
        {
            std::cout<<" create socket errno"<<std::endl;
            exit(SOCKET_ERR);//终止程序
        }
        //2. bind 绑定
        struct sockaddr_in local;//网络通信类型
        //清空
        memset(&local,'\0',sizeof(local));
        local.sin_family=AF_INET;//网络通信
        //htons 主机转网络
        local.sin_port=htons(port_);//端口号
        local.sin_addr.s_addr=INADDR_ANY ; //IP地址

        if(bind(listensock_,(struct sockaddr*)&local,sizeof(local))<0)
        //失败返回-1
        {
           std::cout<<" bind socket errno"<<std::endl;
            exit(BIND_ERR);//终止程序
        }

        // 3.监听
        if(listen(listensock_,backlog)<0)
        {
            //监听失败返回-1
             std::cout<<" listen socket errno"<<std::endl;
            exit(LISTEN_ERR);//终止程序
        }
       
    }
    void start()//启动
    { 
      quit_=false;//服务器没有启动
      while(!quit_)
      {
        //4.获取连接,accept
        struct sockaddr_in client;//网络通信类型
        socklen_t len=sizeof(client);//结构体大小
        int sock=accept(listensock_,(struct sockaddr*)&client,&len);    
        if(sock<0)
        {
            //获取失败
            std::cout<<" accept  errno"<<std::endl;
            continue;//继续执行
        }
        //提取客户端信息
        std::string clientip=inet_ntoa(client.sin_addr);//客户端ip
        uint16_t clientport=ntohs(client.sin_port);//客户端端口号
        //5.获取新连接成功,开始进行业务处理
        std::cout<<"获取新连接成功: "<<sock<<"from "<<listensock_<<std::endl; 
        //service(sock);//多线程版本没有调用函数

       //多线程版本
         pthread_t tid;
         ThreadData*td=new ThreadData(sock,clientip,clientport,this);
         pthread_create(&tid,nullptr,threadRoutine,td);
      }
    }
   static void *threadRoutine(void*args)
    {   
        pthread_detach(pthread_self());//线程分离
        ThreadData*td=(ThreadData*)args;
        td->current->service(td->sock);
        delete td;
        return nullptr;
    }
    void service(int sock)
    {
        char buffer[1024];
        while(true)
        {
            //将sock中的数据读取到buffer中
            ssize_t s=read(sock,buffer,sizeof(buffer)-1);
             if(s>0)
             {
                //读取成功
                buffer[s]=0;
                //使用func 进行回调
                std::string res=func_(buffer);
                std::cout<<res<<std::endl;
                //将res中的数据写给sock中
                write(sock,res.c_str(),res.size());

             }
             else if(s==0)
             {
                //说明对方将连接关闭了
                close(sock);
                std::cout<<"client quit,me too"<<std::endl;
                break; 
             }
             else 
             {
                //读取失败返回-1
                std::cout<<"read errno"<<strerror(errno)<<std::endl;
                break;
             }
        } 
    }
    ~TcpServer()
    {}
      private:
      func_t func_;//函数类型
      int listensock_;//监听套接字 
      bool quit_;//表示服务器是否启动
      uint16_t port_;//端口号
   };
}


tcpServer.cc( 服务端 主函数实现)

#include"tcpServer.hpp"
#include<memory>//智能指针
using namespace std;
using namespace yzq;

static void usage(string proc)
{
    std::cout<<"usage:\n\t"<<proc<<"port\n"<<std::endl;
}

std::string echo(const std::string&message)
{
    return message;
}
// ./tcp_server port
int main(int argc,char*argv[])
{
    //输入两个参数 所以不等于2
    if(argc!=2)
    {
       usage(argv[0]);
       exit(USAGE_ERR);//终止程序
    }
    //将输入的端口号 转化为整数 
    uint16_t port=atoi(argv[1]);
   unique_ptr<TcpServer>tsvr(new TcpServer(echo,port));
   tsvr->initServer();//服务器初始化
   tsvr->start();//启动
    return 0;
}

tcpClient.cc(客户端 不封装)

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

static void usage(string proc)
{
    std::cout<<"usage:\n\t"<<proc<<"port\n"<<std::endl;
}

//./tcp_client  serverip serverport
int main(int argc,char*argv[])
{
   if(argc!=3)
   {
       usage(argv[0]);
       exit(USAGE_ERR);//终止程序
   }
   std::string serverip=argv[1];//IP地址
   uint16_t serverport=atoi(argv[2]);//端口号
   
   //1.创建套接字
   int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
      //创建失败
      cout<<"socket errnr:"<<strerror(errno)<<endl;
      exit(SOCKET_ERR);//终止程序
    }

    //2.发起链接
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));//清空
    server.sin_family=AF_INET;//网络通信类型
    //htons 主机序列转为网络序列
    server.sin_port=htons(serverport);//网络端口号
    inet_aton(serverip.c_str(),&server.sin_addr);//网络IP地址

    int cnt=5;//重连次数
    while(connect(sock,(struct sockaddr*)&server,sizeof(server))!=0)
    {
      //不等于0则链接失败
      sleep(1);
      cout<<"正在尝试重连,重连次数还有:"<<cnt--<<endl;
      if(cnt<=0)
      {
        //没有重连次数
        break;
      }
    }
    if(cnt<=0)
    {
        //链接失败
        cout<<"链接失败.."<<endl;
        exit(SOCKET_ERR);//终止程序
    }

    char buffer[1024];
    //3.链接成功
    while(true)
    {
        string line;
        cout<<"enter>>";
        getline(cin,line);//从cin中获取内容 写入line中
        write(sock,line.c_str(),line.size());//将line中的内容写入到sock文件描述符中   
        ssize_t s=read(sock,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            cout<<"server echo"<<buffer<<endl;
        }
        else if(s==0)
        {
            cout<<"server quit"<<endl;
            break;
        }
        else 
        {
            cout<<"read errno"<<strerror(errno)<<endl;
            break;
        }
    }
    close(sock);
    return 0;
}

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

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

相关文章

Cortex-M3的双堆栈MSP和PSP(学习)

M3的栈&#xff0c;先进后出。 是局部变量内存的开销&#xff0c;函数的调用都离不开栈。 Cortex-M3内核使用了双堆栈&#xff0c;即MSP和PSP。 MSP&#xff1a;Main_Stack_Pointer&#xff0c;即主栈。 PSP&#xff1a;Process_Stack_Pointer&#xff0c;即任务栈。 SP&#…

如何在win7的右键菜单栏上添加“在此处打开Powershell”

打开regedit.exe 找到计算机\HKEY_CLASSES_ROOT\Directory\Background\shell。 在项下建立新项Powershell&#xff0c;并且在Powershell项下建立新项command&#xff0c;如图所示&#xff1a; 在Powershell的默认的项中填写名称在此处打开Powershell窗口。 新建字符串值Ex…

如何快捷发布学生志愿录取情况?

随着新学期的临近&#xff0c;作为一名招生老师&#xff0c;你是否已经做好了新学年的招生准备工作呢&#xff1f;在招生名单确认后&#xff0c;录取查询就成为了当前急需完成的工作。那么&#xff0c;如何让新生能够自主查询自己的录取情况呢&#xff1f; 作为招生老师&#…

flutter开发实战-实现左右来回移动的按钮引导动画效果

flutter开发实战-实现左右来回移动的按钮引导动画效果 最近开发过程中需要实现左右来回移动的按钮引导动画效果 一、动画 AnimationController用来控制一个或者多个动画的正向、反向、停止等相关动画操作。在默认情况下AnimationController是按照线性进行动画播放的。Animati…

CelebA-HQ数据集下载【详细明了版】分辨率包括【64,128,256,512,1024】

CelebA-HQ数据集下载&#xff0c;分辨率包括【64&#xff0c;128&#xff0c;256&#xff0c;512&#xff0c;1024】 前言下载&处理1.下载合并解压img_celeba.7z2.下载list_landmarks_celeba.txt3.获取h5tool.py4.mkdir5. 下载.dat数据 配置环境生成数据集 前言 CelebA-HQ …

谷歌推出Flax:JAX的神经网络库

在优化理论中&#xff0c;损失或成本函数测量拟合或预测值与实际值之间的距离。对于大多数机器学习模型&#xff0c;提高性能意味着最小化损失函数。 但对于深度神经网络&#xff0c;执行梯度下降以最小化每个参数的损失函数可能会消耗大量资源。传统方法包括手动推导和编码&a…

MySQL — InnoDB事务

文章目录 事务定义事务特性事务隔离级别READ UNCOMMITTEDREPEATABLE READREAD COMMITTEDSERIALIZABLE 事务存在的问题脏读&#xff08;Dirty Read&#xff09;不可重复读&#xff08;Non-repeatable Read&#xff09;幻读&#xff08;Phantom Read&#xff09; 事务定义 数据库…

【vue3】elementPlus主题色定制

以scss语言为例 1、element-plus自动按需导入配置&#xff0c;可参考官网按需导入模块 安装element-plus及辅助插件 npm i element-plus --save安装辅助插件 npm install -D unplugin-vue-components unplugin-auto-import安装sass npm i sass -D2、vite.config.js 中配置…

FPGA应用学习笔记--时钟域的控制 亚稳态的解决

时钟域就是同一个时钟的区域&#xff0c;体现在laways语句边缘触发语句中&#xff0c;设计规模增大就会导致时钟不同步&#xff0c;有时差&#xff0c;就要设计多时钟域。 会经过与门的延时产生的新时钟域&#xff0c;这种其实不推荐使用&#xff0c;但在ascl里面很常见 在处理…

《2023年中国企业数字化转型发展白皮书》发布

导读 本报告主要采用市场调查、行业深度访谈、桌面研究等方法&#xff0c;并使用艾媒咨询旗下各大数据计算系统和相关计算模型。 对部分相关的公开信息进行筛选&#xff0c;通过对行业专家、相关企业与网民进行深度访谈&#xff0c;了解相关行业主要情况&#xff0c;获得相应…

k8s dns 解析service异常

查看kube-dns日志 for p in $(kubectl get pods --namespacekube-system -l k8s-appkube-dns -o name); \ do kubectl logs --namespacekube-system $p; done k8s教程&#xff08;service篇&#xff09;-总结_阿甘兄的技术博客_51CTO博客

常用的mysql子查询

你好&#xff01;下面是一些常用的 MySQL 子查询&#xff1a; 标量子查询&#xff08;Scalar Subquery&#xff09;&#xff1a;返回单个值作为查询结果。SELECT column_name FROM table_name WHERE column_name (SELECT column_name FROM table_name WHERE condition); 列表…

ML类CFAR检测器在不同环境中检测性能的分析

摘要&#xff1a;该文是楼主翻阅书籍以及一些论文总结出来的关于ML(均值)类CFAR检测器在不同环境中的性能对比&#xff0c;以及优缺点的总结&#xff0c;可以帮助大家面对不同情形如何选择CFAR问题。由于楼主见识短浅&#xff0c;文中难免出现不足之处&#xff0c;望各位指出。…

Docker之jenkins部署harbor在harbor中完成部署

Docker之jenkins部署harbor在harbor中完成部署 1、harbor作用 Harbor允许用户用命令行工具对容器镜像及其他Artifact进行推送和拉取&#xff0c;并提供了图形管理界面帮助用户查阅和删除这些Artifact。在Harbor 2.0版本中&#xff0c;除容器镜像外&#xff0c;Harbor对符合OCI…

自定义线程池 01 - 阻塞队列

完整代码已上传gitee &#xff0c;地址 &#xff1a;朱元杰的开源仓库 – ThreadPool核心源码仿写 完整文章栏目地址在&#xff1a;Fearless____的博客 - ThreadPool仿写 接下来将手动仿写一个线程池&#xff0c;第一步先仿写 阻塞队列 ​​​​​​​​​​​​​​​​​ 为…

docker删除容器时报错:Error response from daemon: reference does not exist

前言 之前使用的docker版本太低了&#xff0c;升级高版本docker之后的错误。 低版本docker&#xff08;1.30.1&#xff09;中的镜像有&#xff1a;golang、mysql&#xff0c;将docker升级为24.0.5并新拉取mysql最新版本之后&#xff0c;执行docker images命令&#xff0c;发现…

【Kaggle】Identify Contrails to Reduce Global Warming 比赛数据集的可视化(含源代码)

一、数据简单解读 卫星图像最初来自&#xff1a; https://www.goes-r.gov/spacesegment/abi.html高级基线成像仪是GOES-R系列中用于对地球天气、海洋和环境进行成像的主要仪器。ABI用16个不同的光谱波段观察地球&#xff08;上一代GOES只有<>个&#xff09;&#xff0c…

MySQL数据库基础语法 - 上

一&#xff0c;数据库操作 数据库中不区分大小写&#xff01;&#xff01;&#xff01; 1.1 显示数据库 show databases ; 如图&#xff1a; 1.2 创建数据库 create database [ if not exists ]数据库名 ; 如图&#xff1a; 1.3 使用数据库 use 数据库名 &#xff1b; 如图&a…

PHP codeigniter4 搭配Nginx

> 主要是为了用Nginx运行PHP环境 1. Nginx 官方文档的配置 default.conf This configuration enables URLs without “index.php” in them and using CodeIgniter’s “404 - File Not Found” for URLs ending with “.php”. server {listen 80;listen [::]:80;se…

Discovery studio构建药效团(Pharmacophore)的方式

药效团(Pharmacophore)是特征化的三维结构要素的组合&#xff0c;可以分为两种类型。一类是具有相同药理作用的类似物&#xff0c;它们具有某种基本结构&#xff0c;即相同的化学结构部分如磺胺类药物、局麻药、受体阻断剂、拟肾上腺素药物等;另一类是一组化学结构完全不同的分…