一、实验名称

动手打造自己的 IM

二、实验目的

1本次实验旨在锻炼大家的Socket编程能力,以日常生活中广泛使用的IM软件为背景,培养大家对于网络编程的兴趣。

2、通过本次实验,培养linux环境下网络编程能力,使得我们对网络应用层的网络应用软件有了一个深入的理解,这对网络课程的学习起到了很好的实践作用。

三、实验内容及步骤

实验内容:本作业要求同学们完成一个简陋的IM的客户端和服务器端,实现一些最基本和最常用的IM功能:

客户端功能:

1、4个以!开头命令,!login登录命令!list列出在线用户命令!file传输文件命令!logoff注销命令。

2、回显功能:当其他用户登录或者登出时,客户端会收到服务器的提示信息,当向另一个客户端发送文件之后,也会收到该客户端的提示信息。

3、聊天功能:两个客户端可以进行聊天:

以[username]: 开头的行为其他用户向你发送的聊天信息。

以>>[username]: 开头的行为你向其他用户发送的聊天信息。

服务器功能:

Server端基本架构:多进程并发服务器实现,为每个在线用户分配一个进程用以和在线用户保持通信,并在子进程退出(某用户下线)时,回收子进程资源。

ü服务器端固定端口:8081

ü用户登录成功,反馈提示信息

ü维护在线用户信息:包括IP地址和端口号,用户名字,用户密码等

ü向在线用户发出其他用户上/下线提示信息

实验步骤:

1、熟悉Linux下C语言的编程环境和编译方法,学会用gedit来编写并修改代码及命令行工具。

2、学习有关socket方面的有关知识,尝试着去编写服务器端和客户端的实现连接的代码,最重要的是学会使用socket()、listen()、bind()、connect()、accept()等函数的使用方法,在此基础上实现客户端和服务器端的连接。

3、在实现客户端和服务器端连接的基础上编写处理!login、!logoff、!list等简单的命令(无需客户端与客户端交互),这个过程最重要的是学会send()、recv()函数的使用,传递套接字时要注意接收和发送的对应。

4、在实现了单个客户端与服务器的用简单命令交互的基础上,学习线程的有关知识在服务器端创建多线程,实现服务器可以和多个客户端交互的功能。此过程最重要的是学会线程创建和使用,最重要的是pthread_create(4个参数)的使用,其中关键的地方是线程函数的建立及其参数的传递,参数是socket,这样才能在线程中实现与客户端的交互。

5、在实现了多个客户端与服务器进行交互的基础上,就有可能实现客户端与客户端的交互,编程时一个很关键的地方就是所使用的数据结构,没有一个存储logined客户端的数据结构,就很难实现客户端与客户端的交互。因此需要建立一个结构体来存储客户端的socket、port(可选)、username、flag(状态)。

两种方案:

1)从服务器获得另一个客户端的端口号和IP后,让客户端与客户端建立连接,实现直接交互,这就是P2P结构,编程时我试着使用了这种方法,但是连接总是出问题。

2)通过服务器实现客户端之间的交互,简单来说就是客户端把信息通过socket发送到服务器,服务器再把信息通过不同的socket发送到客户端,这是建立在每个客户端都有自己的socket的基础上的。

我选择了第二种方案来实现客户端的交互。

6、编程进行到这里,我突然意识到了如果客户端没有监听服务器的代码,无法接收和识别服务器的信息是客户端要发送的信息、还是服务其本身要发送的信息,思考良久,无奈之下只能大篇幅的修改代码,在客户端使用双线程,一个用于监听键盘的输入,一个用于监听服务器是否发送信息;这样当键盘输入时一个线程接收并负责向服务器发送信息,当收到服务器发送的信息时,另一个线程接收并把信息输出到终端上。

7、在以上的基础上,就可以实现客户端的通话及文件的发送,最后就是调试程序并整理输出的格式。

四、实验分析

1,server.c编译及运行结果:

图1处于监听状态的服务器

2,开启另外终端运行客户端程序:

第一个用户:Me,运行客户端程序之后,使用命令!login登录,成功登录后会收到服务器的发送的信息,结果如下:

图2 一个client登陆成功后的服务器

图3第一个客户端Me登陆成功

另起两个终端第二个客户端You和第三个客户端Him登录后结果如下:

图4多个客户端登陆后的服务器

3,指令!list的使用:

图5 Me客户端上执行!list命令

4,!logoff命令:当用户Me执行!logoff后,客户端显示服务器发送的信息,此时使用!list指令,结果如下:

图6客户端Him执行!logoff命令

图7客户端Him执行!logoff命令后Me执行!list的结果

5,聊天功能演示:

图8 Me客户端和You客户端进行通话

图9You客户端和Me客户端通话

6,指令!file的使用,文件传输功能演示:

图10You客户端向Me发送lilufeng.pdf文件显示成功

图11 Me客户端接收文件成功信息

7、几种错误处理,结果如下:

图12用户名错误登录失败

图13发送文件时用户不在线、发送失败

图14Me想和You对话,You没有登录,连接失败

五、实验结论

难点:

1、本实验是基于Linux环境下,使用C语言编写而成,这两者都需要实验前进行深入的探索和学习,对我而言,加大了编程和实现的难度。

2、对socket和thread方面的知识可以说一无所知,编程的过程中要不断地学习才能向前迈一小步。

感受:

1、这次实验的过程中,我深知自己的知识的匮乏,实验的过程可以说完全是一个探索的过程,对用到的函数我必须要在Dev等工具下先进性一定的应用和学习才能真正掌握其用法和内涵,实践的过程中学到了很多的知识,而且在编程的后面阶段感觉对这些知识已经能够熟练地应用,最有说服力的就是那些函数的掌握,可见,软件这个行业只有在实践中才能真正的学到本领。

2、编程的前后,我的代码被改了很多遍,因为开始编程的时候没有一个整体的构思,只能走一步算一步,每走一步都是那么的艰难,线程的加入和数据结构的改变都使原来的代码结构发生天翻地覆的变化,到最后在客户端加入双线程时整体的代码又改了一遍,所以说这次试验让我感觉非常的累,而且过程中感觉网络编程是那么的复杂,现在想想这些东西,还是很简单的。由此,我明白了开发软件前为什么要进行详细的设计,看着是浪费时间,其实那是在节省时间和精力,而且是开发出一个软件的基础。

3、通过这次试验让我学会了关于网络的socket和P2P等很多知识,这些知识在短期或者说今生难于忘记,当然还有操作系统方面的thread和process方面的知识。

4、同时我也感受到了IT人士的辛劳,其实我一直都在感受到,只不过这次更加强烈罢了,不过辛劳后,看到自己的成果和感到内心的充实,觉得付出还是值得的。

客户端程序源码:

//[浠g爜]

tcpclient.c

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define

SERVER_PORT 8081  //

define the defualt connect port

id

#define

BUFFER_SIZE 255 //buf size

int

clifd,length = 0;//socket size

char

buf[BUFFER_SIZE]; //the buffer of socket

pthread_t

thread;//thread discription

char

command[100],uName[20],pwd[20];

void

*listenServer(int);//listen and receive message from the server

int

cli_socket_connect_server(int);//create socket and connect the

server

int

main(int argc)

{

clifd =

cli_socket_connect_server(SERVER_PORT);//return the socket

while(1)

{ char

frontcmd[20],ch1,ch2;

int countcmd

=0,countFcmd=0;

memset(frontcmd,' ',sizeof(frontcmd));

memset(command,' ',sizeof(command));

memset(buf,' ',sizeof(buf));

while(ch1 =

getchar()) //input the command

{

if(ch1 == ' ')

{

while(ch2 =

getchar())

{

if(ch2 == 'n')

break;

else

command[countcmd++]

= ch2;

}

break;

}

else if(ch1 ==

'n')

break;

else

frontcmd[countFcmd++] = ch1;

}//input

over:frontcmd is the really command

if(frontcmd[0] ==

'>')//talk function process

{

strcpy(buf,">>");

send(clifd,buf,BUFFER_SIZE,0);

char *talkto;

talkto =

strtok(frontcmd,">:");

strcpy(buf,talkto);//talk to whom

strcat(buf,":");//:

is used to strtok in the server

strcat(buf,command);//what you send

send(clifd,buf,BUFFER_SIZE,0);

}

if(strcmp(frontcmd,"!logoff") == 0)//logoff command process

{

strcpy(buf,"!logoff");

send(clifd,buf,BUFFER_SIZE, 0);

strcpy(buf,uName);

send(clifd,buf,BUFFER_SIZE, 0 );

close(clifd);

pthread_exit(NULL);

break;

}//end of logoff

if

if(strcmp(frontcmd,"!list") == 0)//list command process

{

strcpy(buf,"!list");

send(clifd,buf,BUFFER_SIZE, 0 );

}//end of list

if

if(strcmp(frontcmd,"!login") == 0)//login command process

{

strcpy(buf,"!login");

send(clifd,buf,BUFFER_SIZE, 0 );

printf("User

name:t");

scanf("%s",uName);

printf("Password:t");

scanf("%s",pwd);

strcpy(buf,uName);

send(clifd,buf,BUFFER_SIZE, 0 );

length =

recv(clifd,buf,BUFFER_SIZE,0);

if

(length < 0)

{

printf(" error comes

when recieve data from server! send filen");

exit( 1 );

}

printf("From

server:nt%sn",buf);

if(pthread_create(&thread, NULL, (void

*)listenServer, (void *)clifd) == 0) //after login success create

the listenServer thread

{

printf("listen

thread create success!n");

}

else

{

printf("listen

thread create failed!n");

exit(1);

}

}//end of login

if

if(strcmp(frontcmd,"!file") == 0)

{

strcpy(buf,frontcmd);//send front command(!file) to the server

send(clifd,buf,BUFFER_SIZE,0);

char

*file,*toUser;

file =

strtok(command," =>");//file

toUser =

strtok(NULL," =>");//destination user

printf("filename:t%sntoUser:t%s",file,toUser);

strcpy(buf,toUser);//send the file to whom

send(clifd,buf,BUFFER_SIZE,0);

}//end of file

if

}//end of while

return 0;

}

void

*listenServer(int file_socket)

{

while(recv(file_socket,buf,BUFFER_SIZE,0)>0)

{

char *flagcmd =

NULL,*printInfo = NULL;

flagcmd =

strtok(buf,"|");

printInfo =

strtok(NULL,"|");

if(strcmp(flagcmd,"!list") == 0)//list the user that logined

now

printf("from

server:n%s",printInfo);

else

if(strcmp(flagcmd,"!logoff") == 0)//logoff

printf("from

server:nt%s",printInfo);

else

if(strcmp(flagcmd,"!file") == 0)//send the file

{

if(strcmp(printInfo,"0") == 0)//destination user does not login

printf("From

server:ntuser does not login,file failed!n");

else

//destination user logined

printf("nConnect

success!file success!n");

}

else

if(strcmp(flagcmd,"!msg") == 0)//receive the file message

{

printf("From

sever:nt%s",printInfo);

}

else

if(strcmp(flagcmd,"!talk") == 0)//talk message

{

if(strcmp(printInfo,"0") == 0)

printf("The user

does not login!n");

else

printf("%sn",printInfo);

}

else

printf("Please

debug!");

}

}

int

cli_socket_connect_server(int serverPort)

{

struct

sockaddr_in cliaddr;

socklen_t socklen

=  sizeof(cliaddr);

char

sever[20];//store the IP

int

client_socket;

if((client_socket

=  socket(AF_INET,SOCK_STREAM, 0

))  <  0

)//create socket of the client

{

printf(" create

socket error!n ");

exit( 1 );

}

bzero(

& cliaddr,sizeof(cliaddr));

cliaddr.sin_family

=  AF_INET;

inet_aton(sever,

& cliaddr.sin_addr);

cliaddr.sin_port

=  htons(serverPort);//port of

the sever

cliaddr.sin_addr.s_addr  =

htons(INADDR_ANY);//the same IP with the

sever.Connect auto!

if(connect(client_socket,(struct  sockaddr * )

& cliaddr, socklen)

<  0 )//connect

the server

{

printf(" can't

connect!n ");

exit( 1 );

}

return

client_socket; //return the socket

}

服务器端源码:

//tcpserver.c

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define

SERVER_PORT 8081  //

define the defualt connect port

id

#define

LENGTH_OF_LISTEN_QUEUE 10  //

length of listen queue in server

#define

MAX_THREAD 10

#define

BUFFER_SIZE 255

#define

SUCCESS_MESSAGE "Login

Success!"

#define

FAIL_MESSAGE "Login Failed!"

#define

TOTAL_USER_NUM 6

struct

clientInformation

{

int flag;

//record

the struct state: 0 is empty

char loginUsr[20];

//the users that have logined

int socket;

//store the logined uses'

socket

};

struct

clientInformation clientInfo[TOTAL_USER_NUM];//logined users'

array

int

servfd,sever_socket;  //the socket

int len=0;

//record the length of the socket

int numOfThread = 0;

//record the number of

threads

char users[6][10] =

{"Me","You","Him","Her","user","tmd"};//users that can login

char

buf[BUFFER_SIZE];  //the capacity of the

socket

pthread_t

thread[MAX_THREAD]; //thread discription

struct

sockaddr_in servaddr,cliaddr;

void*

docommand(int);//thread function

int main(int

argc)

{

int num;

for(num = 0; num

< TOTAL_USER_NUM; num++)

{

clientInfo[num].flag

= 0;

clientInfo[num].socket = 0;

}

if  ((servfd  =

socket(AF_INET,SOCK_STREAM, 0 ))

<  0 )//create

socket in the server

{

printf(" create socket

error!n ");

exit( 1

);

}

bzero( &

servaddr,sizeof(servaddr));

servaddr.sin_family  =

AF_INET;

servaddr.sin_port  =

htons(SERVER_PORT);

servaddr.sin_addr.s_addr  =

htons(INADDR_ANY);

if(bind(servfd,(struct  sockaddr

* ) & servaddr,sizeof(servaddr)) < 0

)//bind

{

printf(" bind to

port %d failure!n ",SERVER_PORT);

exit( 1

);

}

if(listen(servfd,LENGTH_OF_LISTEN_QUEUE)

<  0

)//listen

{

printf("

call listen failure!n ");

exit( 1

);

}

while(numOfThread

< MAX_THREAD)

{

socklen_t length

=  sizeof(cliaddr);

sever_socket  =

accept(servfd,(struct  sockaddr

* ) & cliaddr, & length);//accept

the login

if

(sever_socket  <

0 )

{

printf("

error comes when call accept!n ");

break;

}

else

{

//void* arg[] =

{&sever_socket};

if(pthread_create(&thread[numOfThread++], NULL,

(void *)docommand, (void *)sever_socket) != 0)

printf("绾跨▼%d鍒涘缓澶辫触!n",numOfThread); //create

the new thread to the login client

else

printf("绾跨▼%d琚垱寤簄",numOfThread);

}

}

close(servfd);

return  0

;

}

void *docommand(int

sever_socket)

{

while

( 1 )

{// server loop will nerver exit unless any body kill the process

memset(buf,' ',sizeof(buf));

len

=  recv(sever_socket, buf,

BUFFER_SIZE, 0);  //recv the

command

if

(len < 0)

{

printf(" error comes when

recieve data from client ! " );

exit( 1 );

}

else

{

if(strcmp(buf,">>") == 0) //talk

process

{

len

=  recv(sever_socket, buf,

BUFFER_SIZE, 0);//recv the user name that the file will be sent

to

if

(len < 0)

{

printf("error comes when

recieve data from client ! " );

exit( 1 );

}

char

temp[100],*talkto;

strcpy(temp,buf);//temp store what will be sent

talkto =

strtok(buf,":");

int j;

for(j = 0;j

< TOTAL_USER_NUM;j++)

{

if(clientInfo[j].flag == 1)

{

if(strcmp(clientInfo[j].loginUsr,talkto) == 0)

{

strcpy(buf,"!talk|");

strcat(buf,temp);//move temp(the information) to buf

send(clientInfo[j].socket,buf,BUFFER_SIZE,0);//send the talking

message to whom what you talk

break;

}

}

}

if(j ==

TOTAL_USER_NUM)

{

strcpy(buf,"!talk");

strcat(buf,"|0");//"0" == user does not login

send(sever_socket,buf,BUFFER_SIZE, 0 ); //send message(user does

not login)

}

}

else

if(strcmp(buf,"!file") == 0)  //file process

{

len

=  recv(sever_socket, buf,

BUFFER_SIZE, 0);//recv the user name that the file will be sent

to

if

(len < 0)

{

printf("error comes when

recieve data from client ! " );

exit( 1 );

}

int i;

for(i = 0;i

< TOTAL_USER_NUM;i++)

{

if(clientInfo[i].flag == 1)

{

if(strcmp(clientInfo[i].loginUsr,buf) == 0)

{

strcpy(buf,"!file|");

strcat(buf,"1");//"1" == user logined

send(sever_socket,buf,BUFFER_SIZE, 0 ); //send to the client who

send file(receiver)

strcpy(buf,"!msg|nreceived the file!n");

send(clientInfo[i].socket,buf,BUFFER_SIZE,0);//tell the client to

recv the file

break;

}

}

}

if(i ==

TOTAL_USER_NUM)

{

strcpy(buf,"!file");

strcat(buf,"|0");//"0" == user does not login

send(sever_socket,buf,BUFFER_SIZE, 0 ); //send message(user does

not login)

}

}//end of if

else

if(strcmp(buf,"!list") == 0)  //list the logined users process

{

strcat(buf,"|");

int k;

for(k = 0; k

< TOTAL_USER_NUM; k++)

{

if(clientInfo[k].flag == 1)

{

strcat(buf,clientInfo[k].loginUsr);

strcat(buf,"n");

}

}

send(sever_socket,buf,BUFFER_SIZE,0); //send all logined users to

client

}//end of else if

1

else

if(strcmp(buf,"!logoff") == 0)  // logoff process

{

while(1)

{

strcat(buf,"|logoff

success!n");

send(sever_socket,buf,BUFFER_SIZE,0); //send logoff success

message

memset(buf,' ',sizeof(buf));

len

=  recv(sever_socket, buf,

BUFFER_SIZE, 0);  //recv the login users name

int k;

for(k = 0; k

< TOTAL_USER_NUM; k++)//move it out from the

clientinformation struct

{

if(strcmp(buf,clientInfo[k].loginUsr) == 0)

{

strcpy(clientInfo[k].loginUsr," ");

clientInfo[k].flag =

0;

clientInfo[k].socket

= 0;

break;

}

}

close(sever_socket);

numOfThread--;

pthread_exit(NULL);

break;

}

}//end of else if

2

else

if(strcmp(buf,"!login") == 0)//login process

{

len

=  recv(sever_socket, buf,

BUFFER_SIZE, 0); //recv the login user's name

if

(len < 0)

{

printf("error comes when

recieve data from client ! login" );

exit( 1 );

}

printf("from

client:%stIP:%stPort:%dn ",buf,

inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));

int i;

for(i = 0;i

< TOTAL_USER_NUM;i++)//judge whether have this

user

{

if(strcmp(buf,users[i]) == 0)

{

for(i = 0;i

< TOTAL_USER_NUM;i++)

{

if(clientInfo[i].flag == 0)

{

strcpy(clientInfo[i].loginUsr,buf);

clientInfo[i].socket

= sever_socket;

clientInfo[i].flag =

1;

break;

}

}

strcpy(buf,SUCCESS_MESSAGE);

send(sever_socket,buf,BUFFER_SIZE, 0 ); //send login success

message

break;

}

}

if(i ==

TOTAL_USER_NUM)

{

printf("No user

called %s! Login failed!n",buf);

memset(buf,' ',sizeof(buf));

strcpy(buf,FAIL_MESSAGE);

send(sever_socket,buf,BUFFER_SIZE, 0 ); //send login failed

message

}

}//end of else if

3

} //end of else

}// end of while

}

又不懂得地方,欢迎留言询问,我必尽力解答!

linux 多线程客户端服务端通信,[转载]多线程实现服务器和客户端、客户端和客户端通信;需要代码,留言...相关推荐

  1. 封装利用libwebsockets写出的客户端、服务端程序为客户端服务端类

    封装利用libwebsockets写出的客户端.服务端程序为客户端服务端类 文章目录 封装利用libwebsockets写出的客户端.服务端程序为客户端服务端类 1.封装 2.封装后写wss客户端.服 ...

  2. 新书预告:《Linux 多线程服务端编程——使用 muduo C++ 网络库》

    看完了 W. Richard Stevens 的传世经典<UNIX 网络编程>, 能照着例子用 Sockets API 编写 echo 服务, 却仍然对稍微复杂一点的网络编程任务感到无从下 ...

  3. Qt:Qt实现Winsock网络编程—TCP服务端和客户端通信(多线程)

    Qt实现Winsock网络编程-TCP服务端和客户端通信(多线程) 前言 感觉Winsock网络编程的api其实和Linux下网络编程的api非常像,其实和其他编程语言的网络编程都差不太多.博主用Qt ...

  4. 《Linux多线程服务端编程:使用muduo C++网络库》书摘6.6.2节

    6.6.2 常见的并发网络服务程序设计方案 W. Richard Stevens 的<UNIX 网络编程(第2 版)>第27 章"Client-ServerDesign Alte ...

  5. 《Linux多线程服务端编程:使用muduoC++网络库》学习笔记

    文章目录 第1章 线程安全的对象生命期管理 1.1 当析构函数遇到多线程 1.1.1 线程安全的定义 1.1.3 线程安全实例 1.2 对象的创建很简单 1.3 销毁很难 1.4 线程安全的Obser ...

  6. 多线程服务端和客户端通信

    ❤️强烈推荐人工智能学习网站❤️ 在不同的机器上可以用TCP进行通信,在同一台机器上也可以,用客户端/服务端模式通信耦合度更低,golang示例多线程服务端和客户端通信,用C++写也可以 packag ...

  7. Linux多线程服务端编程学习(四)finger服务的实现

    源码下载以及安装点击链接https://blog.csdn.net/YoungSusie/article/details/90021742 分类 Muduo网络库编程 学习笔记 例 七步实现finge ...

  8. linux c++ 线程支持 多核应用,linux C++多线程服务端开发

    linux C++多线程服务端开发 UNIX 线程安全的对象生命期管理 当析构函数遇到多线程 构造不要在构造函数中注册任何回调 不要在构造函数中把this传给跨线程的对象 即便在构造函数的最后一行也不 ...

  9. Linux 内核、进程调度、进程通信、多线程、协程

    Linux内核 操作系统是什么 内核是什么 从功能层面上来说,内核就是一个中间层,软件和硬件之间交互的中间层,链接层 从其他方面理解内核 系统调用,开放了很多接口:资源管理 内核实现的策略 宏内核 微 ...

最新文章

  1. 《DSP using MATLAB》示例Example7.23
  2. MyBatis点滴积累
  3. Zookeeper 安装和配置---学习二
  4. 发现在创建云服务器ecs实例的磁盘快照时_玩转ECS第7讲|ECS数据保护-数据备份新特性与最佳实践...
  5. 信息学奥赛C++语言:派送蛋糕
  6. c++ 静态类成员函数(static member function) vs 名字空间 (namespace)
  7. bzoj 2733: [HNOI2012]永无乡(线段树启发式合并)
  8. 中南大学 科学计算和MATLAB 初级语言学习
  9. 计算机用户界面英文,计算机主板CMOS界面英文翻译(2)
  10. 万年历查询,一个wonderful的年历
  11. 一文读懂自然语言处理NLP
  12. 高考英语50分学计算机,高三英语50分到100分学习窍门
  13. 同步、异步ETL架构的比较
  14. 调用PC端、手机、平板摄像头拍照
  15. IDA ,ida pro专业操作手册
  16. lap 加MySQL主从复制_LAP+mysql-主从+redis
  17. html初级入门,HTML初级教程 简介及入门
  18. stm32【 1.3寸LCD屏幕(2)】
  19. PDF malware
  20. 总理写的AI科普书 世界的下一个主宰-人工智能 Karim Massimov

热门文章

  1. 2017年网易校招题 解救小易
  2. tcp连接工具_基于Swoole如何搭建TCP服务,你掌握了吗?
  3. mac 用户 文件夹 权限_Mac视频播放软件推荐
  4. java 随机生成图,Java中的快速实值随机生成器
  5. java 毫秒转分钟和秒_PDF如何转换CAD文件?教你一分钟批量转上百文件方法,看完秒懂!...
  6. c语言vbs,我的vbs整人程序
  7. 计算机二级公共基础知识2020版电子版,2020年计算机二级考试公共基础知识背诵笔记...
  8. oracle segment extent block,Oracle的基础问题segment extent block
  9. python输出子列表_python利用递归函数输出嵌套列表的每个元素
  10. python hstack_Python小白数据科学教程:NumPy (下)