当前位置: 首页 > news >正文

Linux_实现TCP网络通信

目录

1、实现服务器的逻辑 

1.1 socket

1.2 bind

1.3 listen

1.4 accept

1.5 read

1.6 write 

1.7 服务器代码 

2、实现客户端的逻辑

2.1 connect 

2.3 客户端代码

3、实现服务器与客户端的通信

结语 


前言:

        在Linux下,实现传输层协议为TCP的套接字进行网络通信,网络层协议为IPv4,需要用到的接口如下:实现服务器的接口有(socket、bind、listen、accept、read、write),实现客户端的接口有(socket、connect、write、read)。

        实现方式:因为网络通信的本质是进程间通信在云服务器上创建一个服务器进程和一个客户端进程,服务器进程先启动,然后让客户端链接到服务器上,至此客户端可以向服务器发送消息,并且服务器收到消息后可以给对方反馈信息。

        TCP通信示意图如下:

1、实现服务器的逻辑 

        将服务器封装成一个类,服务器的端口号IP地址以及网络描述符(一种类似文件描述符的字段)作为类的成员变量,这样做的好处是对软件实现分层,方便维护代码。按照下文的接口顺序调用实现服务器类。

1.1 socket

        实现网络通信的第一步基本都是调用socket接口,目前已经明确传输层协议为TCP,网络层协议为IPv4,因此可以直接调用socket,该函数介绍如下:

#include <sys/types.h>          
#include <sys/socket.h>int socket(int domain, int type, int protocol);
//domain表示网络层协议,填AF_INET表示IPv4,填AF_INET6表示IPv6
//type表示传输层协议,SOCK_STREAM为TCP,SOCK_DGRAM为UDP
//protocol表示指定特定的协议,默认填0即可//调用成功返回一个类型文件描述符的网络描述符,失败返回-1

1.2 bind

        在网络通信中,bind函数与socket函数密不可分,他是用来绑定网络描述符和地址信息的,目的是让后续的通信可以直接通过网络描述符进行。在使用bind函数前,需要先创建一个包含地址信息的结构体,IPv4对应的结构体类型是struct sockaddr_in,该结构体里需要程序员手动填写3个信息,分别是:1、TCP/UDP,2、自定义的端口号,3、该主机的IP地址。


        体现地址信息的伪代码如下:

struct sockaddr_in local;//定义变量
memset(&local, 0, sizeof(local));//对内部内容清零
local.sin_family = AF_INET;//填写ip协议
local.sin_port = htons(port_);//填写端口号
inet_aton(ip_.c_str(), &(local.sin_addr));//填写ip地址

        其中由于网络传输数据规定以大端字节序传输,因此填写端口号和ip时需要对其进行大端字节序的转换,这里可以调用htons函数和inet_aton函数实现转换,这两个函数的介绍如下:

#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
//将主机字节序转换成网络大端字节序,以返回值的形式转换#include <arpa/inet.h>
int inet_aton(const char *string, struct in_addr*addr);
//将字符串形式的ip转换成网络字节序,并直接赋值给addr指向的空间

        在地址信息完善后,紧接着就是调用bind函数进行绑定,bind函数介绍如下:

int bind(int socket, const struct sockaddr *address,socklen_t address_len);
//socket表示要绑定的网络描述符
//address表示指向的地址信息变量
//address_len表示地址信息变量的大小//调用成功返回0,失败返回-1并设置错误码

1.3 listen

         与UDP不同的是,UDP绑定之后就可以用网络描述符进行接收和发送消息的操作了,但是TCP不一样,TCP的绑定完成后需要对网络描述符再进行监听操作,监听实际上是一种抽象的连接概念(因为TCP是有连接的,而UDP是无连接的,所以需要监听),即调用listen函数。listen函数的介绍如下:

#include <sys/socket.h>  int listen(int sockfd, int backlog);
//sockfd表示要监听的网络描述符
//backlog表示设置套接字的链接队列的最大数量//调用成功返回0,失败返回-1并设置错误码

1.4 accept

        待上述3个函数完成基础工作后,接下来就是最重要的连接环节了,accept函数的作用是和客户端进行连接,但是注意链接不意味着直接接收对方发来的数据,连接完成之后才能开始正常通信。accept函数介绍如下:

#include <sys/socket.h>  int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//sockfd表示服务器的网络描述符,即通过观察该描述符得知是否有客户端链接
//addr是指向地址信息的指针,用来保存客户端的地址信息
//addrlen表示存储客户端地址信息的结构体大小//重要的是调用成功时返回一个新的网络描述符,失败返回-1

         accept的重要之处在于若连接成功,则返回一个新的网络描述符,而该网络描述符才是后续通信的”端口“,意味着现在服务器有两个网络描述符了,一个是socket返回的,一个是accept返回的。可以这样理解他们:socket返回的描述符表示该服务器的总描述符,类似二叉树的根结点,而accept返回的描述符就是用于服务多个客户端的次描述符,即给每个来连接的客户端都分配一个次描述符,类似二叉树的叶子结点。(但是当前服务器的实现只能服务单客户端,因为他不是多线程的)

        题外话:UDP通信只有一个网络描述符,所有的客户端都是通过这一个描述符来进行和服务器的通信

1.5 read

        链接完毕之后,说明此时客户端和服务器双方建立了联系,服务器的下一步就是读取客户端发送的信息,因为TCP是面向字节流的,因此可以使用文件操作的方式进行流读取,即使用read函数读取客户端发送的数据,read函数介绍如下:

#include <unistd.h>  ssize_t read(int fd, void *buf, size_t count);
//fd表示要读取的文件描述符
//buf表示将读取数据存放至缓冲区的缓冲区指针
//count表示读取的字节数

1.6 write 

        服务器不仅仅可以接收数据,还可以向客户端反馈数据,因为accept让服务器和客户端之间有了唯一的网络描述符,因此服务器可以通过该描述符对客户端反馈信息,而无需像UDP需要记录对方的地址信息才可以反馈信息。write函数介绍如下:

#include <unistd.h>  ssize_t write(int fd, const void *buf, size_t count);
//fd表示要写入的文件描述符
//buf表示指向写入的内容
//count表示要写入的字节

1.7 服务器代码 

        实现服务器类的代码如下:

#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";//云服务器默认通信ip
const int backlog = 10; // 但是一般不要设置的太大class TcpServer
{
public:TcpServer(const uint16_t &port, const std::string &ip = defaultip) : listensock_(defaultfd), port_(port), ip_(ip){}void InitServer(){listensock_ = socket(AF_INET, SOCK_STREAM, 0);printf("创建套接字成功, listensock_: %d\n", listensock_);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_);inet_aton(ip_.c_str(), &(local.sin_addr));bind(listensock_, (struct sockaddr *)&local, sizeof(local));printf("绑定套接字成功, listensock_: %d\n", listensock_);// Tcp是面向连接的,服务器一般是比较“被动的”,//服务器一直处于一种,一直在等待连接到来的状态listen(listensock_, backlog);printf("监听套接字成功, listensock_: %d\n", listensock_);}void Start(){printf("TCP服务器正在运行....");for (;;){// 1. 获取新连接struct sockaddr_in client;//保存客户端的地址信息socklen_t len = sizeof(client);//类似recvfromint sockfd = accept(listensock_, (struct sockaddr *)&client, &len);uint16_t clientport = ntohs(client.sin_port);//将客户端端口号拿出来char clientip[32];//将ip从网络字节序转换成主机序列并写进缓冲区clientip内inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));printf("链接到一个客户端, sockfd: %d, client port: %d\n", sockfd, clientport);// 2. 根据新连接来进行通信Service(sockfd);close(sockfd);//关闭文件描述符}}void Service(int sockfd){// 测试代码char buffer[4096];while (true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;std::cout << "客户端说# " << buffer << std::endl;std::string echo_string = "服务器回答# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if(n==0){std::cout<<"读到文件末尾"<<std::endl;sleep(1);break;}else{std::cout<<"读到出错"<<std::endl;sleep(1);break;}}}~TcpServer() {if(listensock_>0) close(listensock_);}private:int listensock_;uint16_t port_;std::string ip_;
};

2、实现客户端的逻辑

        与服务器相比,客户端的实现逻辑要简单许多,因为服务器要将底层的基础设施建设好,而客户端只需要完成连接这一个动作即可,然后就是发送数据和接收反馈,当然,客户端的第一步也是调用socket拿到属于客户端的网络描述符,因为后续的通信工作要通过该描述符进行。

2.1 connect 

        客户端只需要做连接动作即可,而连接动作是依靠connect函数实现的,connect函数介绍如下:

#include <sys/types.h>  
#include <sys/socket.h>  int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd表示要与对方建立连接的客户端网络描述符
//addr指向地址信息的地址,该地址信息即服务器的地址信息
//addrlen表示addr指向的地址信息的大小

        所以调用connect前必须知道服务器的地址信息:即ip地址和端口号,可以通过命令行参数的形式传给客户端代码,并且拿到地址信息后也是要构造struct sockaddr_in变量将信息填写进去。

2.3 客户端代码

        实现客户端代码如下(注意客户端直接在main函数中实现,无需在对其进行封装,因为客户端的代码简单):

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>void Usage(const std::string &proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}//从命令行参数获取服务器的端口和ipstd::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 初始化地址信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);// 将ip转换成网络字节序inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr)); int sockfd = 0;sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字// 客户端发起connect的时候,进行自动随机bind//链接服务器int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){std::cout << "链接失败, reconnect: " << std::endl;sleep(2);}while (true){std::string message;std::cout << "Please Enter# ";std::getline(std::cin, message);int n = write(sockfd, message.c_str(), message.size());//发送数据if (n < 0){std::cout << "write出错" << std::endl;// break;}char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));//接收反馈if (n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}}close(sockfd);return 0;
}

3、实现服务器与客户端的通信

        实现双方通信前,必须满足双方都为进程,但是服务器目前还只是一个类,因此要把服务器类实例化在main函数中,让服务器也变成进程。实现服务器进程的代码如下: 

#include "TCPser.hpp"
#include <iostream>
#include <memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}// 下节课:守护进程化
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(1);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));tcp_svr->InitServer();tcp_svr->Start();return 0;
}

        运行结果:

        特别注意:通信时,必须保证服务器先运行起来,若客户端先运行起来,则会在connect处连接失败,因为找不到对应的服务器端口号。 

结语 

        以上就是关于在Linux下实现TCP网络通信的讲解,实现TCP的主要思路是了解套接字的作用,以及理清对套接字相关函数的调用顺序,并且其次TCP通信的原理。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!! 

相关文章:

Linux_实现TCP网络通信

目录 1、实现服务器的逻辑 1.1 socket 1.2 bind 1.3 listen 1.4 accept 1.5 read 1.6 write 1.7 服务器代码 2、实现客户端的逻辑 2.1 connect 2.3 客户端代码 3、实现服务器与客户端的通信 结语 前言&#xff1a; 在Linux下&#xff0c;实现传输层协议为TCP…...

正则表达式与文本三剑客之grep

目录 前言 一、grep命令 二、基础正则表达式常见元字符 2.1、特殊字符 2.2、定位符 2.3、非打印字符 三、元字符操作实例 3.1、查找特定字符 3.2、利用中括号“[]”来查找集合字符 3.3、查找行首“^”与行尾字符“$” 3.4、查找任意一个字符“.”与重复字符“*” 3.…...

微信小程序开发:项目程序代码构成

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…...

【云原生】Kubernetes微服务Istio:介绍、原理、应用及实战案例

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…...

【Docker】Docker-consul容器服务自动发现与注册

目录 一.Consul概述 1.解决了什么问题 2.什么叫微服务或者注册与发现 3.consul的模式 4.相关命令 二.consul 部署 1.consul服务器部署 2.部署docker容器 3.Nginx负载均衡器 3.1.安装启动nginx 3.2.配置nginx负载均衡 3.3.创建配置consul complate模板文件 3.4.添加…...

Go 1.22 remote error: tls: handshake failure

Golang 1.22 remote error: tls: handshake failure 1.22之前运行下面代码是没有错误 package mainimport ("crypto/tls""fmt""net/http" )func main() {http.DefaultTransport.(*http.Transport).TLSClientConfig &tls.Config{InsecureS…...

迈向通用人工智能:AGI的到来与社会变革展望

正文&#xff1a; 随着科技的飞速发展&#xff0c;通用人工智能&#xff08;AGI&#xff09;的来临似乎已不再遥远。近期&#xff0c;多位行业领袖和专家纷纷预测&#xff0c;AGI的到来时间可能比我们想象的要早。在这篇博客中&#xff0c;我们将探讨AGI的发展趋势、潜在影响以…...

大模型额外篇章三:vercel搭建openai中转服务器

文章目录 一、起因和注意1)起因2)注意二、实现方法(原理:透传)1)nginx方案2)node服务3)纯 js 方案4)选择国外的域名服务商(DNS 解析路径缩短,建议方案国外提供 CDN 云服务商结合自建云服务业务做负载均衡)三、实践(vercel部署OpenAI代理服务器)四、测试搭建的Ope…...

使用 jQuery 中的 this 实例

在 jQuery 中&#xff0c;this 关键字用于表示指向当前操作的 DOM 元素。本篇博客将详细介绍如何在 jQuery 中使用 this 实例。 一、选择器中的 this 在选择器中&#xff0c;this 可以方便地指向当前操作的 DOM 元素。例如&#xff0c;当用户点击一个按钮时&#xff0c;我们想…...

下载最新版Anaconda、安装、更换源、配置虚拟环境并在vscode中使用

文章目录 进入官网进入下载页安装更换源配置虚拟环境env安装包requests在vscode中使用虚拟环境 进入官网 https://repo.anaconda.com/ 或进入清华大学下载 https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/ 进入下载页 安装 更换源 查看已经存在的镜像源 bash cond…...

极狐GitLab Git LFS(大文件存储)如何管理?

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab &#xff1a;https://gitlab.cn/install?channelcontent&utm_sourcecsdn 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署…...

迭代学习笔记

一、迭代学习定义和分类 1、直观理解 迭代学习一般应用于重复性的场景。比如控制一个单自由度的小车以特定的速度曲线移动到指定位置&#xff0c;整个时间是10s&#xff0c;控制频率是0.01&#xff0c;那么整个控制序列就会有1000个点。这1000个点在10s内依次发出&#xff0c…...

【安全】系统安全设计规范(DOC完整版)

1.1安全建设原则 1.2 安全管理体系 1.3 安全管理规范 1.4 数据安全保障措施 1.4.1 数据库安全保障 1.4.2 操作系统安全保障 1.4.3 病毒防治 1.5安全保障措施 1.5.1实名认证保障 1.5.2 接口安全保障 1.5.3 加密传输保障 1.5.4终端安全保障 软件资料清单列表部分文档&…...

windows常用命令整理

本文分享一些常用的windows命令。根据功能的不同&#xff0c;大致可分为以下几个方面&#xff0c;一是文件操作命令&#xff0c;二是进程相关命令&#xff0c;三是磁盘相关命令&#xff0c;四是网络相关命令&#xff0c;五是其他命令。 1.文件操作命令 dir&#xff1a;显示当…...

视频处理基础知识1

1、图像基本知识 图像的组成&#xff1a;像素、RGB(每个像素由三个发光二极管组成)、分辨率&#xff08;横纵向像素的个数乘积&#xff09; PPI每英寸的像素数 DPI每英寸的点数&#xff0c;有可能一个点有多个像素 PPI>300 就属于视网膜级别&#xff0c;就是很清晰&#…...

Linux退不出vim编辑模式

目录 第一章、问题分析1.1&#xff09;报错提示 第二章、解决方式 友情提醒&#xff1a; 先看文章目录&#xff0c;大致了解文章知识点结构&#xff0c;点击文章目录可直接跳转到文章指定位置。 第一章、问题分析 1.1&#xff09;报错提示 报错如下&#xff1a;使用Linux的vi…...

TikTok养号的网络环境及相关代理IP知识

TikTok作为一个流行的短视频分享平台&#xff0c;其用户量非常庞大&#xff0c;很多商家和个人都会使用TikTok来进行引流和推广。由于TikTok的规则和政策限制了每个用户每天发布视频的数量&#xff0c;因此许多用户会使用多个账号来发布更多的视频以提高曝光率。 然而&#xff…...

过程调用和数组的分配访问

系列文章 : 深入理解计算机系统笔记 文章目录 系列文章3.7 过程3.7.1 运行时栈3.7.2 转移控制3.7.3 数据传送3.7.4 栈上的局部存储3.7.5 寄存器中的局部存储空间3.7.6 递归过程 3.8 数组分配和访问3.8.1 基本原则3.8.2 指针运算3.8.3 嵌套的数组3.8.4 定长数组3.8.5 变长数组…...

TeamViewer手机端APP提示:请先验证账户

当你在手机端下载安装了TeamViewerAPP后&#xff0c;需要你先登录个人账号&#xff0c;然后还会要求你验证账户&#xff0c;同时跳转到一个网址中&#xff0c;但是这个网址并没有自动跳转到验证账户的位置。 解决办法&#xff1a; 在手机浏览器中进入下面这个网址&#xff1a;…...

【SpringBoot】分页查询

1. Controller ApiOperation("分页查询")GetMapping("/page")public Result<PageResult> pageResultResult(EmployeePageQueryDTO employeePageQueryDTO) {System.out.println(employeePageQueryDTO.toString());PageResult pageResult employeeSer…...

R语言AI模型部署方案:精准离线运行详解

R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

基于服务器使用 apt 安装、配置 Nginx

&#x1f9fe; 一、查看可安装的 Nginx 版本 首先&#xff0c;你可以运行以下命令查看可用版本&#xff1a; apt-cache madison nginx-core输出示例&#xff1a; nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...

家政维修平台实战20:权限设计

目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系&#xff0c;主要是分成几个表&#xff0c;用户表我们是记录用户的基础信息&#xff0c;包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题&#xff0c;不同的角色&#xf…...

PL0语法,分析器实现!

简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作

一、上下文切换 即使单核CPU也可以进行多线程执行代码&#xff0c;CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短&#xff0c;所以CPU会不断地切换线程执行&#xff0c;从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 在 GPU 上对图像执行 均值漂移滤波&#xff08;Mean Shift Filtering&#xff09;&#xff0c;用于图像分割或平滑处理。 该函数将输入图像中的…...

免费PDF转图片工具

免费PDF转图片工具 一款简单易用的PDF转图片工具&#xff0c;可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件&#xff0c;也不需要在线上传文件&#xff0c;保护您的隐私。 工具截图 主要特点 &#x1f680; 快速转换&#xff1a;本地转换&#xff0c;无需等待上…...

PHP 8.5 即将发布:管道操作符、强力调试

前不久&#xff0c;PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5&#xff01;作为 PHP 语言的又一次重要迭代&#xff0c;PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是&#xff0c;借助强大的本地开发环境 ServBay&am…...

深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏

一、引言 在深度学习中&#xff0c;我们训练出的神经网络往往非常庞大&#xff08;比如像 ResNet、YOLOv8、Vision Transformer&#xff09;&#xff0c;虽然精度很高&#xff0c;但“太重”了&#xff0c;运行起来很慢&#xff0c;占用内存大&#xff0c;不适合部署到手机、摄…...

【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅

目录 前言 操作系统与驱动程序 是什么&#xff0c;为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中&#xff0c;我们在使用电子设备时&#xff0c;我们所输入执行的每一条指令最终大多都会作用到硬件上&#xff0c;比如下载一款软件最终会下载到硬盘上&am…...