【Linux】socket通信编程

socket通信

*
socket简介
socket操作API函数
代码实现

socket简介

网络层的”ip地址”可以唯一标识网络中的主机,而传输层的”端口”可以唯一标识主机中的应用程(进程)。这样利用三元组( ip地址,协议, 端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说”一切皆socket”。TCP/IP协议族包括传输层、网络层、链路层,而socket所在位置如图, Socket是应用层与TCP/IP协议族通信的中间软件抽象层

【Linux】socket通信编程
Socket起源于Unix,而Unix/Linux基本哲学之一就是”一切皆文件”。在许多操作系统中,套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后应用程序以该描述符作为传递参数,通过调用相应函数(如read、write、close等)来完成某种操作(如从套接字中读取或写入数据)。

在生活中,A要电话给B,A拨号,B听到电话铃声后提起电话,这时A和B就建立起了连接,A和B就可以讲话了。等交流结束,挂断电话结束此次交谈。 打电话很简单解释了这工作原理:”open—write/read—close”模式。下面是网络socket通信的基本流程

【Linux】socket通信编程

; socket操作API函数

socket函数

int socket(int domain,int type,int protocol);

参数:
    domain:指定发送通信的域
       可取值:AF_UNIX:本地主机通信,与IPC类似
                    AF_INET:Internet地址IPV4协议
                    AF_INET6:Internet地址IPV6协议
    type:指定socket类型
      可取值:SOCK_STREAM(流套接字)、SOCK_DGRAM(数据报套接字)、SOCK_RAW(原始套接字)
      protocol:指定该套接字描述符上的一个特殊的协议,如TCP,UDP等,一般设为0,会自动选择type的类型对应的协议
    返回值:
        成功:返回创建的套接字描述符
        失败:-1
    补充:SOCK_STREAM(流套接字)应用TCP协议,提供顺序的,可靠的,基于字节流的双向链接
        SOCK_DGRAM(数据报套接字)应用UDP协议,无链接,不可靠,不固定
       SOCK_RAW(原始套接字)提供访问互联网协议和Internal Network Interfaces的权限,只有超级用户才可使用。

connect函数
TCP客户端程序调用socket创建socket_fd后,在调用connect函数来连接服务器.如果客户端这时调用connect函数发出连接请求,服务器端就会接收到这个请求并使accept函数返回,accept函数返回的新的文件描述符就是对应到该客户的TCP连接,通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数
    sockfd: 客户端的socket()创建的描述字
    addr: 要连接的服务器的socket地址信息,这里面包含有服务器的IP地址和端口等信息
    addrlen: socket地址的长度

在调用connect函数之前,还需要设置服务器的ip地址,端口信息到addr去.


    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    inet_aton(SERVER_IP,&serv_addr.sin_addr);

    rv = connect(sockfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    if(rv < 0)
    {
        printf("Failed to connect server:%s\n",SERVER_IP,SERVER_PORT,strerror(errno));
        return -2;
    }
    printf("Connecting to the server[%s:%d] successfully\n",SERVER_IP,SERVER_PORT);

bind函数
调用socket创建socket时,返回的socket描述字存在于协议族空间中,但是没有这个具体的地址,如果想要给它赋值一个地址,就必须调用bind函数.通常服务器在启动的时候都会绑定一个众所周知的地址(ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen函数之前会调用bind函数,而客户端就不会调用,而是在connect函数时由系统随机生成一个。当然客户端也可以在调用connect函数之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数
    sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。

    addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核

    addrlen:对应的是addr的长度。

在调用bind函数之前,还需要设置客户端的ip地址,端口信息到addr去.


    memset(&serv_addr,0 ,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(LISTEN_PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(socket_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))<0)
    {
        printf("create socket failure:%s\n",strerror(errno));
        return -2;
    }
    printf("socket[%d] bind on port [%d] for all ip address ok\n",socket_fd,LISTEN_PORT);

通用套接字sockaddr类型定义

typedef unsigned short int sa_family_t;
struct sockaddr
{
 sa_family_t    sa_family;
 char           sa_data[14];
}

IPv4的套接字sockaddr_in类型定义

typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in_addr
{
 uint32_t s_addr;
};
struct sockaddr_in
{
 sa_family_t        sin_family;
 in_port_t          sin_port;
 struct in_addr     sin_addr;
 unsigned char      sin_zero[8];
};

IPv6的套接字sockaddr_in6类型定义

typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in6_addr
{
 union
 {
 uint8_t __u6_addr8[16];
 uint16_t __u6_addr16[8];
 uint32_t __u6_addr32[4];
 } __in6_u;
}
struct sockaddr_in6 {
 sa_family_t sin6_family;
 in_port_t sin6_port;
 uint32_t sin6_flowinfo;
 struct in6_addr sin6_addr;
 uint32_t sin6_scope_id;
};

Unix域对应的sockaddr_un类型定义

#define UNIX_PATH_MAX 108
struct sockaddr_un
 {
 sa_family_t    sun_family;
 char           sun_path[UNIX_PATH_MAX];
};

listen函数

int listen(int sockfd, int backlog);

参数
    sockefd: socket()系统调用创建的要监听的socket描述字
    backlog: 相应socket可以在内核里排队的最大连接个数

accept函数
TCP服务器端依次调用socket函数、bind函数、listen函数之后,就会监听指定的socket地址了。服务器之后就会调用accpet函数接受来自客户端的连接请求,这个函数默认是一个 阻塞函数,这也意味着如果没有客户端连接服务器的话该程序将 一直阻塞着不会返回,直到有一个客户端连过来为止。一旦客户端调用connect函数就会触发服务器的accept函数返回,这时整个TCP链接就建立好了。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数
    sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字;
    addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等;
    addrlen: 返回客户端协议地址的长度

例如代码


        client_fd = accept(socket_fd,(struct sockaddr*)&cli_addr,&cliaddr_len);
        if(client_fd <0)
        {
             printf("accept new socket failure :%s\n",strerror(errno));
             return -2;
        }

        printf("accept new client[%s:%d] wlth fd [%d]\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),client_fd);

accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。
htons/htonl函数
意思就是把 主机字节序(小端字节序)改为 网络字节序(大端字节序)
最常见的有两种
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址

htons:host network short(2字节/16位) ,端口号16位

htonl:host network long(4字节/32位),IP地址32位

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

参数
    htonl把端口号主机字节序转换为网络字节序
    htons把ip地址主机主机序转换为网络字节序

    ntohl将无符号转换整数从网络字节顺序到主机字节顺序
    ntohs将无符号转换短字符从网络字节顺序到主机字节顺序
    INADDR_ANY指定地址为0.0.0.0地址,表示监听所有的IP地址

    hostlong:主机字节顺序表达的32位数
    hostshort:主机字节顺序表达的16位数

    netlong:一个以网络字节顺序表达的32位数
    netshort:一个以网络字节顺序表达的16位数

serv_addr.sin_port = htons(LISTEN_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

以IP地址127.0.0.1为例

第一步   127   .   0    .   0    .   1                 把IP地址每一部分转换为8位的二进制数。

第二步 01111111.00000000.00000000.00000001 = 2130706433   (主机字节序)

然后把上面的四部分二进制数从右往左按部分重新排列,那就变为:

第三步 00000001.00000000.00000000.01111111 = 16777343        (网络字节序)

以端口12345为例

 第一步     00110000           00111001         =           12345 (主机字节序)
端口号其实就已经是主机字节序了,首先要把端口号写为16位的二进制数
第二步      00111001          00110000          =           14640 (网络字节序)
然后把主机字节序的前八位与后八位调换位置组成新的16位二进制数,这新的16位二进制数就是网络字节序的二进制表示了

因此,如果知道12345端口的网络字节序是14640的话serv_addr.sin_port=htons(12345)可以直接写为 serv_addr.sin_port = htons(14604) 结果是一样的, htons的作用就是把端口号主机字节序转换为网络字节序。

inet_aton和inet_ntoa函数

inet_aton函数将点十进制字符串转换成网络地址 inet_ntoa函数将网络地址转换成点十进制字符串格式

int inet_aton(const char *string, struct in_addr *addr);
将字符串表示的网络地址转换为该地址数值的整数表示,返回的数字总是按照网络字节顺序的

参数描述:
    输入参数string包含ASCII表示的IP地址。
    输出参数addr是将要用新的IP地址更新的结构。
返回值:
  如果这个函数成功,函数的返回值非零。如果输入地址不正确则会返回零。
  使用这个函数并没有错误码存放在errno中,所以他的值会被忽略

char *inet_ntoa(struct in_addr in)
将网络传输的二进制数值转化为成点分十进制的ip地址

返回指向点分十进制字符串的指针。
该函数将一个网络字节顺序的IP地址转换为它所对应的点分十进制串。
注意:对inet_aton的调用传递的是指向结构的指针,而对inet_ntoa的调用传递的是结构本身。

inet_pton/inet_ntop函数
inet_pton:
IP地址转换函数,支持IPv4/IPv6,可以在将IP地址从”点分十进制”转换成网络字节顺序表示Internet地址。
inet_ntop:
IP地址转换函数,支持IPv4/IPv6,可以在将网络字节顺序表示的Internet地址转换成为”点分十进制”

nt inet_pton(int af, const char *src, void *dst);
例如
    inet_pton(AF_INET, "172.20.223.151", &servaddr.sin_addr);
参数
    af:可以是AF_INET(对应的是ipv4)或AF_INET6(ipv6),如果,以不被支持的地址族作为family参数,
    这两个函数都返回一个错误,并将errno置为EAFNOSUPPORT.
    src:是一个指向点分十进制串的指针,
    dst:是一个指向转换后的网络字节序的二进制值的指针。
返回值
    若成功则为1,若输入不是有效的表达式则为0,若出错则为-1,并将errno置为EAFNOSUPPORT.

const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
例如
    inet_ntop(AF_INET, &servaddr.sin_addr, IPdotdec, 16);
参数
    af:可以是AF_INET(对应的是ipv4)或AF_INET6(ipv6),如果,以不被支持的地址族作为family参数,这两个函数都返回一个错误,并将errno置为EAFNOSUPPORT.
    src:是一个指向点分十进制串的指针,
    dst:参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。
    size:他是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC

read函数

ssize_t read(int fd , void *buf,size_t nbytes)
参数:
    sockfd:与远程通信连接的套接字描述符
    buf:接收数据的缓冲区地址
    len:缓冲区长度

read函数是负责从fd中读取内容。当读成功时read返回实际所读的字节数;如果返回的值是0表示已经读到文件的结束了,如果是网络socke fd也就意味着TCP 链接断开了;小于0表示出现了错误并设置错误标志到errno全局变量中,如果错误为EINTR说明读是由中断引起的,如果ECONNREST表示网络连接出了问题
write函数

ssize_t write(int fildes, const void *buf, size_t nbyte);

args:
    int fildes     : 写入文件的文件描述符
    const void *buf: 写入数据在内存空间存储的地址
    size_t nbyte   : 期待写入数据的最大字节数

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。write的返回值大于0,表示写了部分或者全部的数据。返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。

网络I/O操作函数有下面几组:具体参见man文档

 ssize_t read(int fd, void *buf, size_t count);
 ssize_t write(int fd, const void *buf, size_t count);

 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
 ssize_t recv(int sockfd, void *buf, size_t len, int flags);

 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr,socklen_t addrlen);
 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t*addrlen);

 ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
 ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

close/shutdown函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用close函数来关闭一样。close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程

int close(int fd);
int shutdown(int sockfd, int how);

参数
    fd:要关闭的文件
    how:值为 SHUT_RD 则该套接字不可再读入数据了
        SHUT_WR 则该套接字不可再发送数据了
        SHUT_RDWR 则该套接字既不可以读,也不可以写数据了

代码实现

客户端代码

#include
#include
#include
#include
#include
#include
#include
#include
#include

#define SERVER_IP       "127.0.0.1"
#define SERVER_PORT      12345
#define MSG_STR         "Hello beautiful world"

int main (int argc, char **argv)
{
    int                     sockfd  = -1;
    int                     rv      = -1;
    struct sockaddr_in      serv_addr;
    char                    buf[1024];

    sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd < 0)
    {
        printf("Create sockte failure:%s\n",strerror(errno));
        return -1;
    }
    printf("Create sockte [%d] successful!\n",sockfd);

    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    inet_aton(SERVER_IP,&serv_addr.sin_addr);

    rv = connect(sockfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    if(rv < 0)
    {
        printf("Failed to connect server:%s\n",SERVER_IP,SERVER_PORT,strerror(errno));
        return -2;
    }
    printf("Connecting to the server[%s:%d] successfully\n",SERVER_IP,SERVER_PORT);

    while(1)
    {
        rv = write(sockfd,MSG_STR,strlen(MSG_STR));
        if(rv < 0)
        {
            printf("Write to server[%d] successfully:%s\n",sockfd,strerror(errno));
            break;
        }

        memset(buf,0,sizeof(buf));
        rv = read(sockfd,buf,sizeof(buf));
        if(rv <0 )
        {
            printf("Failed to read data server:%s\n",strerror(errno));
            break;
        }
        else if(0==rv)
        {
            printf("Connecting to the server:'%s'\n",rv,buf);
            break;
        }
        printf("read %d bytes bata from server:'%s'\n",rv,buf);
    }
    close(sockfd);
    return 0;
}

服务器端代码

#include
#include
#include
#include
#include
#include
#include
#include

#define LISTEN_PORT     12345
#define BACKLOG         8

int main (int argc, char **argv)
{
    int                 rv = -1;
    int                 socket_fd = -1;
    int                 client_fd = -1;
    struct sockaddr_in  serv_addr;
    struct sockaddr_in  cli_addr;
    socklen_t           cliaddr_len;
    char                buf[1024];
    int                 on = 1;

    socket_fd = socket(AF_INET,SOCK_STREAM,0);
    if(socket_fd < 0)
    {
        printf("create socket failure:%s\n",strerror(errno));
        return -1;
    }
    printf("socket create fd[%d]\n",socket_fd);

    setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

    memset(&serv_addr,0 ,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(LISTEN_PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(socket_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))<0)
    {
        printf("create socket failure:%s\n",strerror(errno));
        return -2;
    }
    printf("socket[%d] bind on port [%d] for all ip address ok\n",socket_fd,LISTEN_PORT);

    listen(socket_fd,BACKLOG);

    while(1)
    {
        printf("\n start wainting and accept new client connect.... \n",socket_fd);

        client_fd = accept(socket_fd,(struct sockaddr*)&cli_addr,&cliaddr_len);
        if(client_fd <0)
        {
             printf("accept new socket failure :%s\n",strerror(errno));
             return -2;
        }

        printf("accept new client[%s:%d] wlth fd [%d]\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),client_fd);

        memset(buf, 0 ,sizeof(buf));
        rv = read(client_fd,buf,sizeof(buf));
        if(rv < 0)
        {
            printf("read data from client socket[%d] failure:%s\n",client_fd ,strerror(errno));
            close(client_fd);
            continue;
        }
        else if(rv == 0)
        {
            printf("client socket[%d] disconnected\n",client_fd);
            close(client_fd);
            continue;
        }
        printf("read %d bytes bata from client[%d] and echo it back :'%s'\n",rv ,client_fd,buf);

        rv = write(client_fd,buf,rv);
        if(rv < 0)
        {
            printf("write %d bytes data back to client[%d] failure:%s\n",rv ,client_fd,strerror(errno));
            close(client_fd);
        }
        printf("close client socket[%d]\n",client_fd);
    }
    close(client_fd);
    close(socket_fd);

    return 0;
}

Original: https://www.cnblogs.com/Ye-Wei/p/16728610.html
Author: 西故黄鹤楼
Title: 【Linux】socket通信编程

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/606318/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • DDoS攻击–TCP攻击概述

    https://blog.csdn.net/qq_34777600/article/details/81945594 posted @2020-12-10 18:07 珠峰之梦 阅…

    Linux 2023年6月7日
    095
  • 命名空间、作用域、LEGB法则、垃圾回收机制

    一、命名空间、作用域、LEGB法则、 1.命名空间和作用域 : &#x3000;&#x3000;&#x547D;&#x540D;&#x7A7…

    Linux 2023年6月8日
    078
  • Linux lsof命令的使用示例

    Linux命令中,lsof代表 Li St Open Files,用于查看所有被打开的文件,同时显示打开文件相对应的进程。Linux/Unix把一切都看做文件(pipes,sock…

    Linux 2023年6月13日
    082
  • 使用docker 5分钟搭建一个博客(mysql+WordPress)

    一.系统环境 二.前言 三.搭建博客 3.1 创建wordpress和mysql容器 3.2 在wordpress界面设置个人博客信息 3.3 WordPress容器创建命令的简化…

    Linux 2023年6月7日
    084
  • Flask的环境配置

    from flask import Flask​ 通过专门的配置文件,读取配置项,适用于配置项较多 settings.py class Config(object):    DEB…

    Linux 2023年6月8日
    092
  • 常用命令记录

    npm仓库查看和修改 npm config set registry https://registry.npm.taobao.org #设置使用淘宝提供的npm仓库 npm con…

    Linux 2023年5月27日
    069
  • IDEA 构建 mybatis 源码

    mybatis 源码构建 mybatis-source 下载 mybatis源码下载 mybatis-parent 版本信息 mybatis-parent 下载 mybatis-p…

    Linux 2023年6月13日
    084
  • linux系统引导过程

    linux系统引导过程 linux-0.11引导时,将依次运行BIOS程序、bootsect.s、setup.s和head.s,完成引导过程后进入到main函数运行。BIOS完成硬…

    Linux 2023年6月13日
    064
  • Kubernetes&Docker集群部署

    集群环境搭建 搭建kubernetes的集群环境 环境规划 集群类型 kubernetes集群大体上分为两类: 一主多从和 多主多从。 一主多从:一台Master节点和多台Node…

    Linux 2023年6月13日
    067
  • css中*{}和*html,body{}的区别

    css里面定义*{padding:0px;margin: 0px;} 相当于选择器,代表html所有的元素,包括html标签、body标签等; {}大括号里面写入需要给定的属性和属…

    Linux 2023年6月13日
    083
  • Linux快速安装流量监控工具(实用版)

    前言: Linux流量监控工具,在此我推荐两种分别为: 1、nload(推荐)因为个人看着舒服点😂 2、iftop 你可以选择上面两种中的任何一种。下面是这两个版本的简介和安装教程…

    Linux 2023年5月27日
    079
  • 详解IP地址、子网掩码、网络号、主机号、网络地址、主机地址

    详解IP地址、子网掩码、网络号、主机号、网络地址、主机地址 概念 IP地址:一般是指逻辑ip; 子网掩码:将IP划分为网络号和主机号的IP; 网络号/主机号:子网掩码转成二进制后,…

    Linux 2023年6月6日
    0102
  • jmeter学习记录–05–Beanshell2

    学习beanshell时有不少的例子、遇到不少问题。在此记录下。 测试实例列表 A1:使用Beanshell请求作为测试请求 一个打包的Jar包,直接对其内的方法进行测试。 第一步…

    Linux 2023年5月28日
    098
  • 【报错解决】【Linux】Name or service not known

    Name or service not known 配置文件位置 /etc/sysconfig/network-scripts/ nano ifcfg-eth0查看网卡配置,确认d…

    Linux 2023年6月14日
    070
  • 天气干燥怎么防止被静电电到

    可以摸一下墙壁或地板,把电放掉,这样摸门把手之类的金属物品就不会被电到了。 可以摸一下墙壁或地板,把电放掉,这样摸门把手之类的金属物品就不会被电到了。亲身实践,十分有效。只是摸墙和…

    Linux 2023年6月6日
    083
  • python爬虫_入门_翻页

    写出来的爬虫,肯定不能只在一个页面爬,只要要爬几个页面,甚至一个网站,这时候就需要用到翻页了 其实翻页很简单,还是这个页面http://bbs.fengniao.com/forum…

    Linux 2023年6月6日
    066
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球