Android ADB 源码分析(三)

前言

之前分析的两篇文章

Android Adb 源码分析(一)

嵌入式Linux:Android root破解原理(二)

 

写完之后,都没有写到相关的实现代码,这篇文章写下ADB的通信流程的一些细节

看这篇文章之前,请先阅读

Linux的SOCKET编程详解 - 江召伟 - 博客园

对socket通信有简单的了解

1、ADB基本通信

理解:

(1)adb的本质,就是socket的通信,通过secket传送数据及文件

(2)adb传送是以每个固定格式的包发送的数据,包的格式如下:

#define A_SYNC 0x434e5953
#define A_CNXN 0x4e584e43
#define A_OPEN 0x4e45504f
#define A_OKAY 0x59414b4f
#define A_CLSE 0x45534c43
#define A_WRTE 0x45545257
#define A_AUTH 0x48545541struct amessage {unsigned command;       /* command identifier constant      */unsigned arg0;          /* first argument                   */unsigned arg1;          /* second argument                  */unsigned data_length;   /* length of payload (0 is allowed) */unsigned data_check;    /* checksum of data payload         */unsigned magic;         /* command ^ 0xffffffff             */
};struct apacket
{apacket *next;unsigned len;unsigned char *ptr;amessage msg;unsigned char data[MAX_PAYLOAD];
};

发送的包格式为apacket格式,其中msg为消息部分,data为数据部分。msg的消息类型有很多种,包括A_SYNCA_CNXNA_OPENA_OKAY等等。

(3)adb给我们预留了调试的信息,我们只需要在adb.h中定义指定的宏,即可看到每次数据的传输过程:

#define DEBUG_PACKETS 1

打开调试信息后,我们可以看到传输过程中的细节,在串口打印里面。

(4)我们使用adb push命令,来跟踪分析下这个apacket数据是怎样传输的:

我们以adb push profile /命令为例,在串口我们可以看见如下详细的传输信息:

status command arg0 arg1 len data

recv: OPEN 00141028 00000000 0006 "sync:."

send: OKAY 0000003e 00141028 0000 ""

recv: WRTE 00141028 0000003e 0009 "STAT..../"

send: OKAY 0000003e 00141028 0000 ""

send: WRTE 0000003e 00141028 0010 "STAT.A......[oHZ"

recv: OKAY 00141028 0000003e 0000 ""

recv: WRTE 00141028 0000003e 0027 "SEND..../profile,33206DATA....2D

send: OKAY 0000003e 00141028 0000 ""

send: WRTE 0000003e 00141028 0008 "OKAY...."

recv: OKAY 00141028 0000003e 0000 ""

recv: WRTE 00141028 0000003e 0008 "QUIT...."

send: OKAY 0000003e 00141028 0000 ""

send: CLSE 00000000 00141028 0000 ""

recv: CLSE 00141028 0000003e 0000 ""

以上recv表示接收的数据包,send表示回传的数据包。后面五个分别为数据包的数据字段值(command arg0 arg1 len data),这样数据我们还是不够直观,我们翻译成更加直接的数据辅以文字解释

这样是不是容易理解多了呢,经过这样的数据发送,我们就通过adb push命令把本地的profile文件推送到远程设备的根目录了。哇..... 原来这么简单,一个profile文件就传输了。流程理解了,我们再来看代码,现在结果你知道了,流程你也懂了,再来看源码,是不是容易理解了呢。

 

2、代码分析

我们看代码也是逆向的看,这样利于我们理解,不会被源码看到晕乎乎,上面流程懂了,知道了每次是以apacket的格式发送的,我们先来研究这个apacket的接收与发送函数。

接收函数handle_packet

void handle_packet(apacket *p, atransport *t)
{asocket *s;D("handle_packet() %c%c%c%c\n", ((char*) (&(p->msg.command)))[0],((char*) (&(p->msg.command)))[1],((char*) (&(p->msg.command)))[2],((char*) (&(p->msg.command)))[3]);print_packet("recv", p);switch(p->msg.command){case A_SYNC:if(p->msg.arg0){send_packet(p, t);if(HOST) send_connect(t);} else {t->connection_state = CS_OFFLINE;handle_offline(t);send_packet(p, t);}return;case A_CNXN: /* CONNECT(version, maxdata, "system-id-string") *//* XXX verify version, etc */if(t->connection_state != CS_OFFLINE) {t->connection_state = CS_OFFLINE;handle_offline(t);}parse_banner((char*) p->data, t);if (HOST || !auth_enabled) {handle_online(t);if(!HOST) send_connect(t);} else {send_auth_request(t);}break;case A_AUTH:if (p->msg.arg0 == ADB_AUTH_TOKEN) {t->key = adb_auth_nextkey(t->key);if (t->key) {send_auth_response(p->data, p->msg.data_length, t);} else {/* No more private keys to try, send the public key */send_auth_publickey(t);}} else if (p->msg.arg0 == ADB_AUTH_SIGNATURE) {if (adb_auth_verify(t->token, p->data, p->msg.data_length)) {adb_auth_verified(t);t->failed_auth_attempts = 0;} else {if (t->failed_auth_attempts++ > 10)adb_sleep_ms(1000);send_auth_request(t);}} else if (p->msg.arg0 == ADB_AUTH_RSAPUBLICKEY) {adb_auth_confirm_key(p->data, p->msg.data_length, t);}break;case A_OPEN: /* OPEN(local-id, 0, "destination") */if (t->online) {char *name = (char*) p->data;name[p->msg.data_length > 0 ? p->msg.data_length - 1 : 0] = 0;s = create_local_service_socket(name);if(s == 0) {send_close(0, p->msg.arg0, t);} else {s->peer = create_remote_socket(p->msg.arg0, t);s->peer->peer = s;send_ready(s->id, s->peer->id, t);s->ready(s);}}break;case A_OKAY: /* READY(local-id, remote-id, "") */if (t->online) {if((s = find_local_socket(p->msg.arg1))) {if(s->peer == 0) {s->peer = create_remote_socket(p->msg.arg0, t);s->peer->peer = s;}s->ready(s);}}break;case A_CLSE: /* CLOSE(local-id, remote-id, "") */if (t->online) {if((s = find_local_socket(p->msg.arg1))) {s->close(s);}}break;case A_WRTE:if (t->online) {if((s = find_local_socket(p->msg.arg1))) {unsigned rid = p->msg.arg0;p->len = p->msg.data_length;if(s->enqueue(s, p) == 0) {D("Enqueue the socket\n");send_ready(s->id, rid, t);}return;}}break;default:printf("handle_packet: what is %08x?!\n", p->msg.command);}put_apacket(p);
}

哇,这个函数好像不复杂

一个函数,然后解析apacket *p数据,根据msg.command的命令值, 然后对应不同的case,有着不同的响应。事实上也就是这样,这个函数主要就是根据不同的消息类型,来处理这个apacket的数据。

下面解析下上面push命令的过程

1、OPEN响应

recv: OPEN 00141028 00000000 0006 "sync:."

send: OKAY 0000003e 00141028 0000 ""

接收到了OPEN的消息,然后附带了一个sync的数据,我们看看是如何响应的。

    case A_OPEN: /* OPEN(local-id, 0, "destination") */if (t->online) {char *name = (char*) p->data;name[p->msg.data_length > 0 ? p->msg.data_length - 1 : 0] = 0;s = create_local_service_socket(name);if(s == 0) {send_close(0, p->msg.arg0, t);} else {s->peer = create_remote_socket(p->msg.arg0, t);s->peer->peer = s;send_ready(s->id, s->peer->id, t);s->ready(s);}}break;

调用create_local_service_socket(“sync”);

fd = service_to_fd(name);

//创建本地socket,并为这个socket创建数据处理线程file_sync_service

ret = create_service_thread(file_sync_service, NULL);

//把这个本地socket关联到结构asocket *s

s = create_local_socket(fd);

调用create_remote_socket(p->msg.arg0, t); //把远程的socket也与这个结构体asocket 关联。

如上两个函数调用,主要是初始化本地的socket对,本地socket用来跟后台服务线程之间的通信,以及跟对应命令的后台服务线程通信。初始化adb通信的环境。其中asocket *s为本地socket与远程socket的一个关联结构体,其中s保存的是本地socket的信息,s->peer保存的是远程socket相关的信息。

send_ready(s->id, s->peer->id, t); 然后发送OKAY给PC端。

static void send_ready(unsigned local, unsigned remote, atransport *t)
{D("Calling send_ready \n");apacket *p = get_apacket();p->msg.command = A_OKAY;p->msg.arg0 = local;p->msg.arg1 = remote;send_packet(p, t);
}

这个与我们看到的流程相符合。接收到OPEN的消息,初始化一些状态,然后返回一个OKAY的状

2、WRITE响应

recv: WRTE 00141028 0000003e 0009 "STAT..../"

send: OKAY 0000003e 00141028 0000 ""

send: WRTE 0000003e 00141028 0010 "STAT.A......[oHZ"

接收到了WRITE的消息,顺带了一个查询STAT的数据,我们看看是如何响应的:

    case A_WRTE:if (t->online) {if((s = find_local_socket(p->msg.arg1))) {unsigned rid = p->msg.arg0;p->len = p->msg.data_length;if(s->enqueue(s, p) == 0) {D("Enqueue the socket\n");send_ready(s->id, rid, t);}return;}}break;

先通过参数p->msg.arg1找到我们在OPEN的时候建立的结构体信息asocket *s, 然后处理本地socket队列中的数据(s为本地,s->peer为远程)

s->enqueue(s, p)即为之前 关联的函数local_socket_enqueue,其在create_local_socket(fd); 的时候设置。

static int local_socket_enqueue(asocket *s, apacket *p)
{D("LS(%d): enqueue %d\n", s->id, p->len);p->ptr = p->data;/* if there is already data queue'd, we will receive** events when it's time to write.  just add this to** the tail*/if(s->pkt_first) {goto enqueue;}/* write as much as we can, until we** would block or there is an error/eof*/while(p->len > 0) {int r = adb_write(s->fd, p->ptr, p->len);if(r > 0) {p->len -= r;p->ptr += r;continue;}if((r == 0) || (errno != EAGAIN)) {D( "LS(%d): not ready, errno=%d: %s\n", s->id, errno, strerror(errno) );s->close(s);return 1; /* not ready (error) */} else {break;}}if(p->len == 0) {put_apacket(p);return 0; /* ready for more data */}enqueue:p->next = 0;if(s->pkt_first) {s->pkt_last->next = p;} else {s->pkt_first = p;}s->pkt_last = p;/* make sure we are notified when we can drain the queue */fdevent_add(&s->fde, FDE_WRITE);return 1; /* not ready (backlog) */
}

我们通过adb_write(s->fd, p->ptr, p->len)把要处理的数据,写入到本地socket对应的fd中,等待处理。

然后调用send_ready(s->id, rid, t);返回一个OKAY的状态

我们把待处理的数据adb_write之后,又是在哪里处理的呢,我们之前在创建本地socket的时候,就创建了一个线程,对应的处理socket数据的函数file_sync_service。

我们来看看file_sync_service函数是如何处理的

void file_sync_service(int fd, void *cookie)
{syncmsg msg;char name[1025];unsigned namelen;char *buffer = malloc(SYNC_DATA_MAX);if(buffer == 0) goto fail;for(;;) {D("sync: waiting for command\n");if(readx(fd, &msg.req, sizeof(msg.req))) {fail_message(fd, "command read failure");break;}namelen = ltohl(msg.req.namelen);if(namelen > 1024) {fail_message(fd, "invalid namelen");break;}if(readx(fd, name, namelen)) {fail_message(fd, "filename read failure");break;}name[namelen] = 0;msg.req.namelen = 0;D("sync: '%s' '%s'\n", (char*) &msg.req, name);switch(msg.req.id) {case ID_STAT:if(do_stat(fd, name)) goto fail;break;case ID_LIST:if(do_list(fd, name)) goto fail;break;case ID_SEND:if(do_send(fd, name, buffer)) goto fail;break;case ID_RECV:if(do_recv(fd, name, buffer)) goto fail;break;case ID_QUIT:goto fail;default:fail_message(fd, "unknown command");goto fail;}}fail:if(buffer != 0) free(buffer);D("sync: done\n");adb_close(fd);
}

原来在这里处理的数据,终于找到你, 我们收到的消息是查看路径是否存在,这里对应的就是ID_STAT,还有其他的消息处理,比如ID_SEND,ID_RECV,ID_QUIT,望文生义,我们就不具体解释了。我们还是看看ID_STAT对应的处理吧do_stat(fd, name)。

static int do_stat(int s, const char *path)
{syncmsg msg;struct stat st;msg.stat.id = ID_STAT;if(lstat(path, &st)) {msg.stat.mode = 0;msg.stat.size = 0;msg.stat.time = 0;} else {msg.stat.mode = htoll(st.st_mode);msg.stat.size = htoll(st.st_size);msg.stat.time = htoll(st.st_mtime);}return writex(s, &msg.stat, sizeof(msg.stat));
}

这里就是判断路径是否存在的逻辑了,这个就是我们想要的,我们把判断的结果存储在msg.stat, 然后把对应的结果写回去writex。

我们把检测的状态writex之后,但是这个数据还没有发送回PC端啊,是在哪里发送回去的呢,我们继续跟踪

我们在create_local_socket创建本地socket的时候,顺便还注册了一个回调函数local_socket_event_func

static void local_socket_event_func(int fd, unsigned ev, void *_s)
{asocket *s = _s;D("LS(%d): event_func(fd=%d(==%d), ev=%04x)\n", s->id, s->fd, fd, ev);/* put the FDE_WRITE processing before the FDE_READ** in order to simplify the code.*/if(ev & FDE_WRITE){apacket *p;while((p = s->pkt_first) != 0) {while(p->len > 0) {int r = adb_write(fd, p->ptr, p->len);if(r > 0) {p->ptr += r;p->len -= r;continue;}if(r < 0) {/* returning here is ok because FDE_READ will** be processed in the next iteration loop*/if(errno == EAGAIN) return;if(errno == EINTR) continue;}D(" closing after write because r=%d and errno is %d\n", r, errno);s->close(s);return;}if(p->len == 0) {s->pkt_first = p->next;if(s->pkt_first == 0) s->pkt_last = 0;put_apacket(p);}}/* if we sent the last packet of a closing socket,** we can now destroy it.*/if (s->closing) {D(" closing because 'closing' is set after write\n");s->close(s);return;}/* no more packets queued, so we can ignore** writable events again and tell our peer** to resume writing*/fdevent_del(&s->fde, FDE_WRITE);s->peer->ready(s->peer);}if(ev & FDE_READ){apacket *p = get_apacket();unsigned char *x = p->data;size_t avail = MAX_PAYLOAD;int r;int is_eof = 0;while(avail > 0) {r = adb_read(fd, x, avail);D("LS(%d): post adb_read(fd=%d,...) r=%d (errno=%d) avail=%d\n", s->id, s->fd, r, r<0?errno:0, avail);if(r > 0) {avail -= r;x += r;continue;}if(r < 0) {if(errno == EAGAIN) break;if(errno == EINTR) continue;}/* r = 0 or unhandled error */is_eof = 1;break;}D("LS(%d): fd=%d post avail loop. r=%d is_eof=%d forced_eof=%d\n",s->id, s->fd, r, is_eof, s->fde.force_eof);if((avail == MAX_PAYLOAD) || (s->peer == 0)) {put_apacket(p);} else {p->len = MAX_PAYLOAD - avail;r = s->peer->enqueue(s->peer, p);D("LS(%d): fd=%d post peer->enqueue(). r=%d\n", s->id, s->fd, r);if(r < 0) {/* error return means they closed us as a side-effect** and we must return immediately.**** note that if we still have buffered packets, the** socket will be placed on the closing socket list.** this handler function will be called again** to process FDE_WRITE events.*/return;}if(r > 0) {/* if the remote cannot accept further events,** we disable notification of READs.  They'll** be enabled again when we get a call to ready()*/fdevent_del(&s->fde, FDE_READ);}}/* Don't allow a forced eof if data is still there */if((s->fde.force_eof && !r) || is_eof) {D(" closing because is_eof=%d r=%d s->fde.force_eof=%d\n", is_eof, r, s->fde.force_eof);s->close(s);}}if(ev & FDE_ERROR){/* this should be caught be the next read or write** catching it here means we may skip the last few** bytes of readable data.*/
//        s->close(s);D("LS(%d): FDE_ERROR (fd=%d)\n", s->id, s->fd);return;}
}

我们看后面if(ev & FDE_READ)部分:

adb_read(fd, x, avail);把数据读出来,然后调用r = s->peer->enqueue(s->peer, p);,即把数据发送给远程socket的队列处理。(s->speer即远程端,之前已经说明)

s->peer->enqueue函数即remote_socket_enqueue

static int remote_socket_enqueue(asocket *s, apacket *p)
{D("entered remote_socket_enqueue RS(%d) WRITE fd=%d peer.fd=%d\n",s->id, s->fd, s->peer->fd);p->msg.command = A_WRTE;p->msg.arg0 = s->peer->id;p->msg.arg1 = s->id;p->msg.data_length = p->len;send_packet(p, s->transport);return 1;
}

这样我们就把STAT的结果,通过WRITE返回给了PC端

这个与我们看到的流程也是相符的,接收到WRITE(STAT)的消息,先返回一个OKAY的状态,在返回WRITE(STAT)的结果。

我们可以观察之前的数据接收及发送流程,可以发现每次一个WRITE消息,后面都是返回一个OKAY WRITE消息。

贴了这么多的代码,是不是有点晕了,再贴就真的看不下去了,我们下面重新来理一理思路。

1. adb其实就是个socket通信,数据发过来发过去。

2. adb每次都是发送的一个数据包,数据结构是struct apacket,其中包含msg消息部分,及data数据部分。

3. 从PC跟device通信的过程,有一条协议流程,通过不断的数据交互发送,实现数据文件传递。

4. 我们可以定义 #define DEBUG_PACKETS 1 这样可以看到socket通信的数据发送过程。

5. socket数据建立传输过程,会创建socket,创建事件监听线程,注册回调响应函数,乱七八糟的....

 

如果觉得不错,请关注公众号【嵌入式Linux】,谢谢

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

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

相关文章

A Generative Adversarial Network-based Deep Learning Method for Low-quality Defect ImageReconstructi

A Generative Adversarial Network-based Deep Learning Method for Low-quality Defect ImageReconstruction and Recognition 中文名&#xff1a;基于生成对抗网络的深度低质量缺陷图像的学习方法重建与认可 主体思路概述&#xff1a;将模糊的图像使用Gan进行生成与重建&…

bootstrap3 表单构建器_FastReport.NET报表设计器连接到OracleDB关系数据库

首先&#xff0c;您可以使用ODBC连接器。但是它充满了很多设置。FastReport.NET报表设计器连接到OracleDB关系数据库如您所见&#xff0c;您需要创建数据源及其连接字符串。动作比较多。此方法的替代方法是使用FastReport中的连接器。实际上&#xff0c;有两个连接器可供选择。…

码农,选好你的老板

[01 两个小故事] 小明2009年毕业 那一年刚好是金融危机之后&#xff0c;全球经济还不是很好&#xff0c;找到一份好的工作非常困难。 好在&#xff0c;小明误打误撞面试上华为一份码农的工作。 刚开始那几年&#xff0c;小明很拼命&#xff0c;不断的在解bug和制造bug中自由…

Automated defect inspection system for metal surfaces based on deep learning and data augmentation

Automated defect inspection system for metal surfaces based on deep learning and data augmentation 基于深度学习和数据增强的金属表面缺陷自动检测系统 简述&#xff1a;卷积变分自动编码器(CVAE)生成特定的图像&#xff0c;再使用基于深度CNN的缺陷分类算法进行分类。在…

python第七章_python 第七章 模块

模块 一个py文件就是一个模块 模块一共三种&#xff1a;1.python标准库 2.第三方模块 3.应用程序自定义模块 import&#xff1a;1.执行对应文件 2.引入变量名 if__name__"__main__": #1.用于被调用文件测试 2.防止主程序被调用 time模块 常用命令 时间模块 1 importt…

openwrt 音频开发

1、Linux 音频架构图 音视频的好坏 直接影响 产品体验 2、音频架构图层次说明 openWRT 采用 ALSA 层次图,如下 Application: 上层应用 主要调用alsa-lib 中的接口 实现业务逻辑。使用alsa-util中aplay,arecord,amixer,speaker-test进行相关测试。HAL层 : 移植alsa-lib 和 a…

Automatic Detection of Welding Defects Using Faster R-CNN

Automatic Detection of Welding Defects Using Faster R-CNN 基于快速R-CNN的焊接缺陷自动检测 简介&#xff1a;使用Inception-ResNet模型进行缺陷检测 数据集&#xff1a;射线图像 Abstract Experts are required to properly detect the test results and it takes a lot…

让Android Studio支持系统签名(证书)

有时候&#xff0c;我们开发的apk需要用到系统权限&#xff0c;需要在AndroidManifest.xml中添加共享系统进程属性&#xff1a; android:sharedUserId"android.uid.system" android:sharedUserId"android.uid.shared" android:sharedUserId"android…

eslint 保存自动格式化_代码规范之理解ESLint、Prettier、EditorConfig

授权转载自&#xff1a;nowThenhttps://juejin.cn/post/6895889063111294990前言团队多人协同开发项目中困恼团队管理一个很大的问题是&#xff1a;无可避免地会出现每个开发者编码习惯不同、代码风格迥异&#xff0c;为了代码高可用、可维护性&#xff0c; 如何从项目管理上尽…

Deep learning based multi-scale channel compression feature surface defect detection system

基于深度学习的多尺度通道压缩特征表面缺陷检测系统 Deep learning based multi-scale channel compression feature surface defect detection system 简述&#xff1a;首先应用背景分割和模板匹配技术来定义覆盖目标工件的ROI区域。提取的感兴趣区域被均匀地裁剪成若干个图像…

前端MVC框架之 Angular

一、什么是Angular jQuery&#xff0c;它属于一种类库(一系列函数的集合)&#xff0c;以DOM为驱动核心&#xff1b;而Angular是一种 MVC 的前端框架&#xff0c;则是前端框架&#xff0c;以数据和逻辑为驱动核心&#xff0c;它有着诸多特性&#xff0c;最重要的是&#xff1a;模…

C语言关键字(三)

之前的两篇文章 嵌入式Linux&#xff1a;c语言深度解剖&#xff08;数据类型关键字&#xff09;​zhuanlan.zhihu.com 嵌入式Linux&#xff1a;c语言深度解剖&#xff08;入门篇&#xff09;​zhuanlan.zhihu.com 这篇文件继续讲解C语言关键字 想问大家一个问题&#xff0c…

python bottle框架 运维_python bottle 框架实战教程:任务管理系统 V_1.0版 | linux系统运维...

经过1-2个星期的开发&#xff0c;现在用任务管理功能&#xff08;添加、删除、修改&#xff0c;详细&#xff09;、项目管理功能&#xff08;添加、删除&#xff0c;修改&#xff0c;详细&#xff09;等&#xff0c;我把现在完成的版本&#xff0c;叫做1.0吧。发布完这个版本后…

form 窗体增加边框_C#控件美化之路(13):美化Form窗口(上)

在开发中最重要的就是美化form窗口&#xff0c;在开发中&#xff0c;大多都是用会用自主美化的窗口开发程序。本文只是点多&#xff0c;分为上中下节。分段讲解。本文主要讲解窗口美化关键步骤。首先美化窗体&#xff0c;就需要自己绘制最大化 最小化 关闭按钮。其次就是界面样…

第四周数据结构

转载于:https://www.cnblogs.com/bgd150809329/p/6650255.html

gdb x命令_gdb基本命令

参考自&#xff1a;gdb基本命令(非常详细)_JIWilliams-CSDN博客_gdb命令​blog.csdn.net本文介绍使用gdb调试程序的常用命令。 GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。如果你是在 UNIX平台下做软件&#xff0c;你会发现GDB这个调试工具有比VC、BCB的图形化调试…

YOLOX-PAI: An Improved YOLOX, Stronger and Faster than YOLOv6

YOLOX-PAI&#xff1a;一种改进的YOLOX&#xff0c;比YOLOv6更强更快 原文&#xff1a;https://arxiv.org/pdf/2208.13040.pdf 代码&#xff1a;https://github.com/alibaba/EasyCV 0.Abstract We develop an all-in-one computer vision toolbox named EasyCV to facilita…

安装一直初始化_3D max 软件安装问题大全

纵使3D虐我千百遍&#xff0c;我待3D如初恋&#xff01;大家好&#xff0c;我是小文。快节奏生活的今天&#xff0c;好不容易有点学习的热情&#xff0c;打开电脑学习下&#xff0c;没想到被简单的软件安装问题浇灭&#xff01;这不是耽误了一位伟大的世界设计师诞生的节奏吗&a…

TCP/IP 协议栈 -- 编写UDP客户端注意细节

上节我们说到了TCP 客户端编写的主要细节&#xff0c; 本节我们来看一下UDP client的几种情况&#xff0c;测试代码如下&#xff1a; server&#xff1a; #include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>…

RuntimeError: Address already in use

问题描述&#xff1a;Pytorch用多张GPU训练时&#xff0c;会报地址已被占用的错误。其实是端口号冲突了。 因此解决方法要么kill原来的进程&#xff0c;要么修改端口号。 在代码里重新配置 torch.distributed.init_process_group()dist_init_method tcp://{master_ip}:{mast…