socket编程(C 知识分享:Socket 编程详解,万字长文)

时间:2023-12-05 11:33:16 阅读:4

C++知识分享:Socket 编程详解,万字长文



先容


Socket编程让你懊丧吗?从man pages中很难取得有效的信息吗?你想跟上年代去编Internet干系的步骤,但是为你在调用 connect() 前的bind() 的布局而不知所措?等等…


幸而我以前将这些事完成了,我将和一切人共享我的知识了。假如你了解 C 言语并想穿过网络编程的沼泽,那么你来对场合了。


读者目标


这个文档是一个指南,而不是参考书。假如你刚开头 socket 编程并想找一本入门书,那么你是我的读者。但这不是一本完全的 socket 编程书。


平台和编译器


这篇文档中的大大多代码都在 Linux 平台PC 上用 GNU 的 gcc 告捷编译过。并且它们在 HPUX平台 上用 gcc 也告捷编译过。但是注意,并不是每个代码片断都独立测试过。


目次:


1) 什么是套接字?


2) Internet 套接字的两品种型


3) 网络实际


4) 布局体


5) 本机转换


6) IP 地点和怎样处理它们


7) socket()函数


8) bind()函数


9) connect()函数


10) listen()函数


11) accept()函数


12) send()和recv()函数


13) sendto()和recvfrom()函数


14) close()和shutdown()函数


15) getpeername()函数


16) gethostname()函数


17) 域名办事(DNS)


18) 客户-办事器背景知识


19) 简便的办事器


20) 简便的客户端


21) 数据报套接字Socket


22) 壅闭


23) select()--多路同步I/O


24) 参考材料


1)什么是 socket?


你常常听到人们议论着 “socket”,大概你还不晓得它的确切涵义。如今让我报告你:它是使用标准Unix 文件形貌符 (file descriptor) 和别的步骤通讯的办法。 什么? 你约莫听到一些Unix妙手(hacker)如此说过:“呀,Unix中的统统就是文件!”谁人家伙约莫正在说到一个内幕:Unix 步骤在实行任何情势的 I/O 的时分,步骤是在读大概写一个文件形貌符。一个文件形貌符只是一个和掀开的文件干系联的整数。但是(注意后方的话),这个文件约莫是一个网络毗连,FIFO,管道,终端,磁盘上的文件大概什么别的的东西。Unix 中一切的东西就是文件!以是,你想和Internet上别的步骤通讯的时分,你将要使用到文件形貌符。你必需了解刚刚的话。如今你脑海中大概冒出如此的动机:“那么我从何处取得网络通讯的文件形貌符呢?”,这个成绩无论怎样我都要回复:你使用体系调用 socket(),它前往套接字形貌符 (socket descriptor),然后你再经过它来举行send() 和 recv()调用。


“但是...”,你约莫有很大的疑惑,“假如它是个文件形貌符,那么为什 么不必寻常调用read()和write()来举行套接字通讯?”简便的答案是:“你可以使用!”。具体的答案是:“你可以,但是使用send()和recv()让你更好的控制数据传输。”


存在如此一个情况:在我们的天下上,有很多种套接字。有DARPA Internet 地点 (Internet 套接字),当地节点的途径名 (Unix套接字),CCITT X.25地点 (你可以将X.25 套接字完全忽略)。约莫在你的Unix 机器上另有别的的。我们在这里只讲第一种:Internet 套接字。


2)Internet 套接字的两品种型


什么意思?有两品种型的Internet 套接字?是的。不,我在扯谎。但是另有很多,但是我可不想吓着你。我们这里只讲两种。除了这些, 我方案别的先容的 "Raw Sockets" 也好坏常强壮的,很值得查阅。
那么这两品种型是什么呢?一种是"Stream Sockets"(流格式),别的一种是"Datagram Sockets"(数据包格式)。我们今后谈到它们的时分也会用到 "SOCK_STREAM" 和 "SOCK_DGRAM"。数据报套接字偶尔也叫“无毗连套接字”(假如你的确要毗连的时分可以用connect()。) 流式套接字是可靠的双向通讯的数据流。假如你向套接字按排序输入“1,2”,那么它们将按排序“1,2”抵达另一边。它们是无错误的转达的,有本人的错误控制,在此不讨论。


有什么在使用流式套接字?你约莫听说过 telnet,不是吗?它就使用流式套接字。你必要你所输入的字符按排序抵达,不是吗?相反,WWW欣赏器使用的 HTTP 协议也使用它们来下载页面。实践上,当你经过端口80 telnet 到一个 WWW 站点,然后输入 “GET pagename” 的时分,你也可以取得 HTML 的内容。为什么流式套接字可以到达高质量的数据传输?这是由于它使用了“传输控制协议 (The Transmission Control Protocol)”,也叫 “TCP” (请参考 RFC-793 取得具体材料。)TCP 控制你的数据按排序抵达并且没有错误。你约莫听到 “TCP” 是由于听到过 “TCP/IP”。这里的 IP 是指“Internet 协议”(请参考 RFC-791。) IP只是处理 Internet 路由罢了。


那么数据报套接字呢?为什么它叫无毗连呢?为什么它是不成靠的呢?有如此的一些内幕:假如你发送一个数据报,它约莫会抵达,它约莫序次颠倒了。假如它抵达,那么在这个包的内里是无错误的。数据报也使用 IP 作路由,但是它不使用 TCP。它使用“用户数据报协议 (User Datagram Protocol)”,也叫 “UDP” (请参考 RFC-768。)


为什么它们是无毗连的呢?主要是由于它并不象流式套接字那样维持一个毗连。你只需创建一个包,布局一个有目标信息的IP 头,然后发射去。无需毗连。它们通常使用于传输包-包信息。简便的使用步骤有:tftp, bootp等等。


你约莫会想:“假定命据丧失了这些步骤怎样正常事情?”我的伙伴,每个步骤在 UDP 上有本人的协议。比如,tftp 协议每发射的一个被接遭到包,收到者必需发回一个包来说“我收到了!” (一个“下令准确应对”也叫“ACK” 包)。假如在一定时间内(比如5秒),发送方没有收到应对,它将重新发送,直到取得 ACK。这一ACK历程在完成 SOCK_DGRAM 使用步骤的时分十分紧张。


3)网络实际


既然我刚刚提到了协议层,那么如今是讨论网络毕竟怎样事情和一些 关于 SOCK_DGRAM 包是怎样创建的例子。固然,你也可以跳过这一段, 假如你以为以前熟习的话。


如今是学习数据封装 (Data Encapsulation) 的时分了!它十分十分紧张。它紧张性紧张到你在网络课程学习中无论怎样也得也得把握它(图1:数据封装)。主要 的内容是:一个包,先是被第一个协议(在这里是TFTP )在它的报头(约莫 是报尾)包装(“封装”),然后,整个数据(包含 TFTP 头)被别的一个协议 (在这里是 UDP )封装,然后下一个( IP ),不休反复下去,直到硬件(物理) 层( 这里是以太网 )。
当别的一台机器吸收到包,硬件先剥去以太网头,内核剥去IP和UDP 头,TFTP步骤再剥去TFTP头,最初取得数据。


如今我们终于讲到声名狼藉的网络分层模子 (Layered Network Model)。这种网络模子在形貌网络体系上相对别的模子有很多优点。比如, 你可以写一个套接字步骤而不必体贴数据的物理传输(串行口,以太网,连 接单位接口 (AUI) 照旧别的介质),由于底层的步骤会为你处理它们。实践 的网络硬件和拓扑关于步骤员来说是纯透的。


不说别的空话了,我如今列出整个条理模子。假如你要到场网络测验, 可一定要记取:


使用层 (Application)


表现层 (Presentation)


会话层 (Session)


传输层(Transport)


网络层(Network)


数据链路层(Data Link)


物理层(Physical)


物理层是硬件(串口,以太网等等)。使用层是和硬件层相隔最远的--它 是用户和网络交互的场合。 这个模子云云通用,假如你想,你可以把它作为修车指南。把它对应 到 Unix,后果是:


使用层(Application Layer) (telnet, ftp,等等)


传输层(Host-to-Host Transport Layer) (TCP, UDP)


Internet层(Internet Layer) (IP和路由)


网络拜候层 (Network Access Layer) (网络层,数据链路层和物理层)


如今,你约莫看到这些条理怎样和谐来封装原始的数据了。


看看创建一个简便的数据包有几多事情?哎呀,你将不得不使用 "cat" 来创建数据包头!这仅仅是个打趣。关于流式套接字你要作的是 send() 发 送数据。关于数据报式套接字,你依照你选择的办法封装数据然后使用 sendto()。内核将为你创建传输层和 Internet 层,硬件完成网络拜候层。 这就是古代科技。 如今完毕我们的网络实际速成班。哦,忘记报告你关于路由的事变了。 但是我禁绝备谈它,假如你真的体贴,那么参考 IP RFC。


4)布局体


终于谈到编程了。在这章,我将谈到被套接字用到的种种数据典范。 由于它们中的一些内容很紧张了。


起首是简便的一个:socket形貌符。它是底下的典范:


int


仅仅是一个稀有的 int。


从如今起,事变变得不成思议了,而你所需做的就是持续看下去。注 意如此的内幕:有两种字节分列排序:紧张的字节 (偶尔叫 "octet",即八 位位组) 在前方,大概不紧张的字节在前方。前一种叫“网络字节排序 (Network Byte Order)”。有些机器在内里是依照这个排序储存数据,而别的 一些则不然。当我说某数据必需依照 NBO 排序,那么你要调用函数(比如 htons() )来将它从本机字节排序 (Host Byte Order) 转换过去。假如我没有 提到 NBO, 那么就让它坚持本机字节排序。


我的第一个布局(在这个武艺手册TM中)--struct sockaddr.。这个布局 为很多典范的套接字储存套接字地点信息:


struct sockaddr {   unsigned short sa_family; /* 地点家属, AF_xxx */   char sa_data[14]; /*14字节协议地点*/ };



sa_family 可以是种种千般的典范,但是在这篇文章中都是 "AF_INET"。 sa_data包含套接字中的目标地点和端口信息。这仿佛有点 不明智。


为了处理struct sockaddr,步骤员创造了一个并列的布局: struct sockaddr_in ("in" 代表 "Internet"。)


struct sockaddr_in {   short int sin_family; /* 通讯典范 */   unsigned short int sin_port; /* 端口 */   struct in_addr sin_addr; /* Internet 地点 */   unsigned char sin_zero[8]; /* 与sockaddr布局的长度相反*/ };



用这个数据布局可以轻松处理套接字地点的基本元素。注意 sin_zero (它被到场到这个布局,并且长度和 struct sockaddr 一样) 应该使用函数 bzero() 或 memset() 来全部置零。 同时,这一紧张的字节,一个指向 sockaddr_in布局体的指针也可以被指向布局体sockaddr并且代替它。如此的话即使 socket() 想要的是 struct sockaddr *,你仍旧可以使用 struct sockaddr_in,并且在最初转换。同时,注意 sin_family 和 struct sockaddr 中的 sa_family 一律并可以设置为 "AF_INET"。最初,sin_port和 sin_addr 必需是网络字节排序 (Network Byte Order)!


你约莫会反对道:"但是,怎样让整个数据布局 struct in_addr sin_addr 依照网络字节排序呢?" 要晓得这个成绩的答案,我们就要仔细的看一看这 个数据布局: struct in_addr, 有如此一个团结 (unions):


/* Internet 地点 (一个与汗青有关的布局) */ struct in_addr {   unsigned long s_addr; };



它以前是个最坏的团结,但是如今那些日子已往了。假如你声明 "ina" 是数据布局 struct sockaddr_in 的实例,那么 "ina.sin_addr.s_addr" 就储 存4字节的 IP 地点(使用网络字节排序)。假如你不幸的体系使用的照旧恐 怖的团结 struct in_addr ,你照旧可以安心4字节的 IP 地点并且和外表 我说的一样(这是由于使用了“#define”。)


5)本机转换


我们如今到了新的章节。我们以前讲了很多网络到本机字节排序的转 换,如今可以实践了! 你可以转换两品种型: short (两个字节)和 long (四个字节)。这个函 数关于变量典范 unsigned 也实用。假定你想将 short 从本机字节排序转 换为网络字节排序。用 "h" 表现 "本机 (host)",接着是 "to",然后用 "n" 表 示 "网络 (network)",最初用 "s" 表现 "short": h-to-n-s, 大概 htons() ("Host to Network Short")。


太简便了... ,假如不是太傻的话,你一定想到了由"n","h","s",和 "l"构成的准确 组合,比如这里一定没有stolh() ("Short to Long Host") 函数,不仅在这里 没有,一切场合都没有。但是这里有:


htons()--"Host to Network Short"


htonl()--"Host to Network Long"


ntohs()--"Network to Host Short"


ntohl()--"Network to Host Long"


如今,你约莫想你以前晓得它们了。你也约莫想:“假如我想改动 char 的排序要怎样办呢?” 但是你约莫立刻就想到,“用不着思索的”。你约莫 会想到:我的 68000 机器以前使用了网络字节排序,我没有必要去调用 htonl() 转换 IP 地点。你约莫是对的,但是当你移植你的步骤到别的机器 上的时分,你的步骤将失败。可移植性!这里是 Unix 天下!记取:在你 将数据放到网络上的时分,确信它们是网络字节排序的。


最初一点:为什么在数据布局 struct sockaddr_in 中, sin_addr 和 sin_port 必要转换为网络字节排序,而sin_family 需不必要呢? 答案是: sin_addr 和 sin_port 分散封装在包的 IP 和 UDP 层。因此,它们必必要 是网络字节排序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据布局中包含什么典范的地点,以是它必需是本机字节排序。同时, sin_family 没有发送到网络上,它们可以是本机字节排序。


6)IP 地点和怎样处理它们


如今我们很侥幸,由于我们有很多的函数来便利地利用 IP 地点。没有 必要用手工盘算它们,也没有必要用"<<"利用来储存发展整字型。 起首,假定你以前有了一个sockaddr_in布局体ina,你有一个IP地 址"132.241.5.10"要储存在此中,你就要用到函数inet_addr(),将IP地点从 点数格式转换成无标记长整型。使用办法如下:


ina.sin_addr.s_addr = inet_addr("132.241.5.10");



注意,inet_addr()前往的地点以前是网络字节格式,以是你无需再调用 函数htonl()。 我们如今发觉外表的代码片断不好坏常完备的,由于它没有错误反省。 不言而喻,当inet_addr()产生错误时前往-1。记取这些二进制数字?(无符 号数)-1仅仅和IP地点255.255.255.255切合合!这但是广播地点!大错特 错!记取要优秀行错误反省。


好了,如今你可以将IP地点转换发展整型了。有没有其相反的办法呢? 它可以将一个in_addr布局体输入成点数格式?如此的话,你就要用到函数 inet_ntoa()("ntoa"的涵义是"network to ascii"),就像如此:


printf("%s",inet_ntoa(ina.sin_addr));



它将输入IP地点。必要注意的是inet_ntoa()将布局体in-addr作为一个参数,不是长整形。相反必要注意的是它前往的是一个指向一个字符的 指针。它是一个由inet_ntoa()控制的静态的安稳的指针,以是每次调用 inet_ntoa(),它就将掩盖前次调用时所得的IP地点。比如:


char *a1, *a2; …… a1 = inet_ntoa(ina1.sin_addr); /* 这是198.92.129.1 */ a2 = inet_ntoa(ina2.sin_addr); /* 这是132.241.5.10 */ printf("address 1: %s\n",a1); printf("address 2: %s\n",a2); 输入如下: address 1: 132.241.5.10 address 2: 132.241.5.10



假定你必要保存这个IP地点,使用strcopy()函数来指向你本人的字符 指针。


外表就是关于这个主题的先容。稍后,你将学习将一个类 似"wintehouse.gov"的字符串转换成它所对应的IP地点(查阅域名办事,稍 后)。


7)socket()函数


我想我不克不及再不提这个了-底下我将讨论一下socket()体系调用。


底下是具体先容:


#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);



但是它们的参数是什么? 起首,domain 应该设置成 "AF_INET",就 象外表的数据布局struct sockaddr_in 中一样。然后,参数 type 报告内核 是 SOCK_STREAM 典范照旧 SOCK_DGRAM 典范。最初,把 protocol 设置为 "0"。(注意:有很多种 domain、type,我不成能逐一列出了,请看 socket() 的 man协助。固然,另有一个"更好"的办法去取得 protocol,同 时请查阅 getprotobyname() 的 man 协助。) socket() 只是前往你今后在体系调用种约莫用到的 socket 形貌符,或 者在错误的时分前往-1。全局变量 errno 中将储存前往的错误值。(请参考 perror() 的 man 协助。)


8)bind()函数


一旦你有一个套接字,你约莫要将套接字和机器上的一定的端口关联 起来。(假如你想用listen()来侦听一定端口的数据,这是必要一步--MUD 告 诉你说用下令 "telnet x.y.z 6969"。)假如你只想用 connect(),那么这个步 骤没有必要。但是无论怎样,请持续读下去。


这里是体系调用 bind() 的约莫:


#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, struct sockaddr *my_addr, int addrlen);



sockfd 是调用 socket 前往的文件形貌符。my_addr 是指向数据布局 struct sockaddr 的指针,它保存你的地点(即端口和 IP 地点) 信息。 addrlen 设置为 sizeof(struct sockaddr)。 简便得很不是吗? 再看看例子:


#include <string.h> #include <sys/types.h> #include <sys/socket.h> #define MYPORT 3490 main() {   int sockfd;   struct sockaddr_in my_addr;   sockfd = socket(AF_INET, SOCK_STREAM, 0); /*必要错误反省 */   my_addr.sin_family = AF_INET; /* host byte order */   my_addr.sin_port = htons(MYPORT); /* short, network byte order */   my_addr.sin_addr.s_addr = inet_addr("132.241.5.10");   bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */   /* don't forget your error checking for bind(): */   bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));   ……



这里也有要注意的几件事变。my_addr.sin_port 是网络字节排序, my_addr.sin_addr.s_addr 也是的。别的要注意到的事变是因体系的不同, 包含的头文件也不尽相反,请查阅当地的 man 协助文件。 在 bind() 主题中最初要说的话是,在处理本人的 IP 地点和/或端口的 时分,有些事情是可以主动处理的。


my_addr.sin_port = 0; /* 随机选择一个没有使用的端口 */


my_addr.sin_addr.s_addr = INADDR_ANY; /* 使用本人的IP地点 */


经过将0赋给 my_addr.sin_port,你报告 bind() 本人选择切合的端 口。相反,将 my_addr.sin_addr.s_addr 设置为 INADDR_ANY,你报告 它主动填上它所运转的机器的 IP 地点。


假如你从来警惕审慎,那么你约莫注意到我没有将 INADDR_ANY 转 换为网络字节排序!这是由于我晓得内里的东西:INADDR_ANY 实践上就 是 0!即使你改动字节的排序,0仍然是0。但是完善主义者说应该到处一 致,INADDR_ANY大概是12呢?你的代码就不克不及事情了,那么就看底下 的代码:


my_addr.sin_port = htons(0); /* 随机选择一个没有使用的端口 */


my_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 使用本人的IP地点 */


你大概不信赖,外表的代码将可以任意移植。我只是想指出,既然你 所碰到的步骤不会都运利用用htonl的INADDR_ANY。


bind() 在错误的时分仍然是前往-1,并且设置全局错误变量errno。


在你调用 bind() 的时分,你要警惕的另一件事变是:不要接纳小于 1024的端标语。一切小于1024的端标语都被体系保存!你可以选择从1024 到65535的端口(假如它们没有被别的步骤使用的话)。
你要注意的别的一件小事是:偶尔分你基本不必要调用它。假如你使 用 connect() 来和长程机器举行通讯,你不必要体贴你的当地端标语(就象 你在使用 telnet 的时分),你只需简便的调用 connect() 就可以了,它会检 查套接字对否绑定端口,假如没有,它会本人绑定一个没有使用的当地端 口。


9)connect()步骤


如今我们假定你是个 telnet 步骤。你的用户下令你取得套接字的文件 形貌符。你听从下令调用了socket()。下一步,你的用户报告你经过端口 23(标准 telnet 端口)毗连到"132.241.5.10"。你该怎样做呢? 侥幸的是,你正在阅读 connect()--怎样毗连到长程主机这一章。你可 不想让你的用户扫兴。


connect() 体系调用是如此的:


#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);



sockfd 是体系调用 socket() 前往的套接字文件形貌符。serv_addr 是 保存着目标地端口和 IP 地点的数据布局 struct sockaddr。addrlen 设置 为 sizeof(struct sockaddr)。 想晓得得更多吗?让我们来看个例子:


#include <string.h> #include <sys/types.h> #include <sys/socket.h> #define DEST_IP "132.241.5.10" #define DEST_PORT 23 main() {   int sockfd;   struct sockaddr_in dest_addr; /* 目标地点*/   sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误反省 */   dest_addr.sin_family = AF_INET; /* host byte order */   dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */   dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);   bzero(&(dest_addr.sin_zero),; /* zero the rest of the struct */   /* don't forget to error check the connect()! */   connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));   ……



再一次,你应该反省 connect() 的前往值--它在错误的时分前往-1,并 设置全局错误变量 errno。 同时,你约莫看到,我没有调用 bind()。由于我不在乎当地的端标语。 我只体贴我要去那。内核将为我选择一个切合的端标语,而我们所毗连的 场合也主动地取得这些信息。统统都不必担心。


10)listen()函数


是换换内容得时分了。假定你不渴望与长程的一个地点相连,大概说, 仅仅是将它踢开,那你就必要等候接入哀求并且用种种办法处理它们。处 理历程分两步:起首,你听--listen(),然后,你承受--accept() (请看底下的 内容)。


除了要一点表明外,体系调用 listen 也相当简便。


int listen(int sockfd, int backlog);



sockfd 是调用 socket() 前往的套接字文件形貌符。backlog 是在进入 行列中允许的毗连数目。什么意思呢? 进入的毗连是在行列中不休等候直 到你承受 (accept() 请看底下的文章)毗连。它们的数目限定于行列的允许。 大大多体系的允许数目是20,你也可以设置为5到10。


和别的函数一样,在产生错误的时分前往-1,并设置全局错误变量 errno。


你约莫想象到了,在你调用 listen() 前你大提要调用 bind() 大概让内 核任意选择一个端口。假如你想侦听进入的毗连,那么体系调用的排序可 能是如此的:


socket();


bind();


listen();


/* accept() 应该在这 */


由于它相当的明白,我将在这里不给出例子了。(在 accept() 那一章的 代码将愈加完全。)真正贫苦的局部在 accept()。


11)accept()函数


准备好了,体系调用 accept() 会有点乖僻的场合的!你可以想象产生 如此的事变:有人从很远的场合经过一个你在侦听 (listen()) 的端口毗连 (connect()) 到你的机器。它的毗连将到场到等候承受 (accept()) 的行列 中。你调用 accept() 报告它你有空闲的毗连。它将前往一个新的套接字文 件形貌符!如此你就有两个套接字了,原本的一个还在侦听你的谁人端口, 新的在准备发送 (send()) 和吸收 ( recv()) 数据。这就是这个历程!


函数是如此界说的:


#include <sys/socket.h> int accept(int sockfd, void *addr, int *addrlen);



sockfd 相当简便,是和 listen() 中一样的套接字形貌符。addr 是个指 向局部的数据布局 sockaddr_in 的指针。这是要求接入的信息所要去的地 方(你可以测定谁人地点在谁人端口召唤你)。在它的地点转达给 accept 之 前,addrlen 是个局部的整形变量,设置为 sizeof(struct sockaddr_in)。 accept 将不会将多余的字节给 addr。假如你放入的少些,那么它会经过改 变 addrlen 的值反应出来。


相反,在错误时前往-1,并设置全局错误变量 errno。


如今是你应该熟习的代码片断。


#include <string.h> #include <sys/socket.h> #include <sys/types.h> #define MYPORT 3490 /*用户接入端口*/ #define BACKLOG 10 /* 几多等候毗连控制*/ main() {   int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */   struct sockaddr_in my_addr; /* 地点信息 */   struct sockaddr_in their_addr; /* connector's address information */   int sin_size;   sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误反省*/   my_addr.sin_family = AF_INET; /* host byte order */   my_addr.sin_port = htons(MYPORT); /* short, network byte order */   my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */   bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */   /* don't forget your error checking for these calls: */   bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));   listen(sockfd, BACKLOG);   sin_size = sizeof(struct sockaddr_in);   new_fd = accept(sockfd, &their_addr, &sin_size);   ……



注意,在体系调用 send() 和 recv() 中你应该使用新的套接字形貌符 new_fd。假如你只想让一个毗连过来,那么你可以使用 close() 去关闭原 来的文件形貌符 sockfd 来制止同一个端口更多的毗连。


12)send() and recv()函数


这两个函数用于流式套接字大概数据报套接字的通讯。假如你喜好使 用无毗连的数据报套接字,你应该看一看底下关于sendto() 和 recvfrom() 的章节。


send() 是如此的:


int send(int sockfd, const void *msg, int len, int flags);



sockfd 是你想发送数据的套接字形貌符(大概是调用 socket() 大概是 accept() 前往的。)msg 是指向你想发送的数据的指针。len 是数据的长度。 把 flags 设置为 0 就可以了。(具体的材料请看 send() 的 man page)。 这里是一些约莫的例子:


char *msg = "Beej was here!"; int len, bytes_sent; …… len = strlen(msg); bytes_sent = send(sockfd, msg, len, 0); ……



send() 前往实践发送的数据的字节数--它约莫小于你要求发送的数 目! 注意,偶尔分你报告它要发送一堆数据但是它不克不及处理告捷。它只是 发送它约莫发送的数据,然后渴望你可以发送别的的数据。记取,假如 send() 前往的数据和 len 不婚配,你就应该发送别的的数据。但是这里也 有个好消息:假如你要发送的包很小(小于约莫 1K),它约莫处理让数据一 次发送完。最初要说得就是,它在错误的时分前往-1,并设置 errno。


recv() 函数很相似:


int recv(int sockfd, void *buf, int len, unsigned int flags);



sockfd 是要读的套接字形貌符。buf 是要读的信息的缓冲。len 是缓 冲的最大长度。flags 可以设置为0。(请参考recv() 的 man page。) recv() 前往实践读入缓冲的数据的字节数。大概在错误的时分前往-1, 同时设置 errno。


很简便,不是吗? 你如今可以在流式套接字上发送数据和吸收数据了。 你如今是 Unix 网络步骤员了!


13)sendto() 和 recvfrom()函数


“这很不错啊”,你说,“但是你还没有讲无毗连数据报套接字呢?” 没成绩,如今我们开头这个内容。 既然数据报套接字不是毗连到长程主机的,那么在我们发送一个包之 前必要什么信息呢? 不错,是目标地点!看看底下的:


int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);



你以前看到了,除了别的的两个信息外,其他的和函数 send() 是一样 的。 to 是个指向数据布局 struct sockaddr 的指针,它包含了目标地的 IP 地点和端口信息。tolen 可以简便地设置为 sizeof(struct sockaddr)。 和函数 send() 相似,sendto() 前往实践发送的字节数(它也约莫小于 你想要发送的字节数!),大概在错误的时分前往 -1。


相似的另有函数 recv() 和 recvfrom()。recvfrom() 的界说是如此的:


int recvfrom(int sockfd, void *buf, int len, unsigned int flags,   struct sockaddr *from, int *fromlen);



又一次,除了两个增长的参数外,这个函数和 recv() 也是一样的。from 是一个指向局部数据布局 struct sockaddr 的指针,它的内容是源机器的 IP 地点和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用前往后,fromlen 保存着实践储存在 from 中的地点的长度。


recvfrom() 返吸收到的字节长度,大概在产生错误后前往 -1。


记取,假如你用 connect() 毗连一个数据报套接字,你可以简便的调 用 send() 和 recv() 来满意你的要求。这个时分仍然是数据报套接字,依 然使用 UDP,体系套接字接口会为你主动加上了目标和源的信息。


14)close()和shutdown()函数


你以前整天都在发送 (send()) 和吸收 (recv()) 数据了,如今你准备关 闭你的套接字形貌符了。这很简便,你可以使用寻常的 Unix 文件形貌符 的 close() 函数:


close(sockfd);



它将避免套接字上更多的数据的读写。任安在另一端读写套接字的企 图都将前往错误信息。假如你想在怎样关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定朝向上的通讯大概双向的通讯(就象close()一 样)关闭,你可以使用:


int shutdown(int sockfd, int how);



sockfd 是你想要关闭的套接字文件形貌复。how 的值是底下的此中之 一:


0 – 不允许承受


1 – 不允许发送


2 – 不允许发送和承受(和 close() 一样)


shutdown() 告捷时前往 0,失败时前往 -1(同时设置 errno。) 假如在无毗连的数据报套接字中使用shutdown(),那么只不外是让 send() 和 recv() 不克不及使用(记取你在数据报套接字中使用了 connect 后 是可以使用它们的)。


15)getpeername()函数


这个函数太简便了。 它太简便了,致使我都不想单列一章。但是我照旧如此做了。 函数 getpeername() 报告你在毗连的流式套接字上谁在别的一边。函 数是如此的:


#include <sys/socket.h> int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);



sockfd 是毗连的流式套接字的形貌符。addr 是一个指向布局 struct sockaddr (大概是 struct sockaddr_in) 的指针,它保存着毗连的另一边的 信息。addrlen 是一个 int 型的指针,它初始化为 sizeof(struct sockaddr)。 函数在错误的时分前往 -1,设置相应的 errno。


一旦你取得它们的地点,你可以使用 inet_ntoa() 大概 gethostbyaddr() 来打印大概取得更多的信息。但是你不克不及取得它的帐号。(假如它运转着愚 蠢的保卫历程,这是约莫的,但是它的讨论以前超出了本文的范围,请参 考 RFC-1413 以取得更多的信息。)


16)gethostname()函数


乃至比 getpeername() 还简便的函数是 gethostname()。它前往你程 序所运转的机器的主机名字。然后你可以使用 gethostbyname() 以取得你 的机器的 IP 地点。


底下是界说:


#include <unistd.h> int gethostname(char *hostname, size_t size);



参数很简便:hostname 是一个字符数组指针,它将在函数前往时保存 主机名。size是hostname 数组的字节长度。


函数调用告捷时前往 0,失败时前往 -1,并设置 errno。


17)域名办事(DNS)


假如你不晓得 DNS 的意思,那么我报告你,它代表域名办事(Domain Name Service)。它主要的功效是:你给它一个容易影象的某站点的地点, 它给你 IP 地点(然后你就可以使用 bind(), connect(), sendto() 大概别的 函数) 。当一一局部输入:


$ telnet whitehouse.gov


telnet 能晓得它将毗连 (connect()) 到 "198.137.240.100"。 但是这是怎样事情的呢? 你可以调用函数 gethostbyname():


#include <netdb.h> struct hostent *gethostbyname(const char *name);



很明白的是,它前往一个指向 struct hostent 的指针。这个数据布局 是如此的:


struct hostent {   char *h_name;   char **h_aliases;   int h_addrtype;   int h_length;   char **h_addr_list; }; #define h_addr h_addr_list[0]



这里是这个数据布局的具体材料:


h_name – 地点的正式称呼。


h_aliases – 空字节-地点的准备称呼的指针。


h_addrtype –地点典范; 通常是AF_INET。


h_length – 地点的比专长度。


h_addr_list – 零字节-主机网络地点指针。网络字节排序。


h_addr - h_addr_list中的第一地点。


gethostbyname() 告捷时前往一个指向布局体 hostent 的指针,大概 是个空 (NULL) 指针。(但是和从前不同,不设置errno,h_errno 设置错 误信息,请看底下的 herror()。) 但是怎样使用呢? 偶尔分(我们可以从电脑手册中发觉),向读者贯注 信息是不够的。这个函数可不象它看上去那么难用。


这里是个例子:


#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> int main(int argc, char *argv[]) {   struct hostent *h;   if (argc != 2) { /* 反省下令行 */   fprintf(stderr,"usage: getip address\n");   exit(1);   }   if ((h=gethostbyname(argv[1])) == NULL) { /* 取得地点信息 */   herror("gethostbyname");   exit(1);   }   printf("Host name : %s\n", h->h_name);   printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr))); return 0; }



在使用 gethostbyname() 的时分,你不克不及用 perror() 打印错误信息 (由于 errno 没有使用),你应该调用 herror()。


相当简便,你只是转达一个保存机器名的字符串(比如 "whitehouse.gov") 给 gethostbyname(),然后从前往的数据布局 struct hostent 中获取信息。


唯一约莫让人不解的是输入 IP 地点信息。h->h_addr 是一个 char *, 但是 inet_ntoa() 必要的是 struct in_addr。因此,我转换 h->h_addr 成 struct in_addr *,然后取得数据。


18)客户-办事器背景知识


这里是个客户--办事器的天下。在网络上的一切东西都是在处理客户进 程和办事器历程的扳谈。举个telnet 的例子。当你用 telnet (客户)经过23 号端口登岸到主机,主机上运转的一个步骤(寻常叫 telnetd,办事器)激活。 它处理这个毗连,体现登岸界面,等等。


图 2 分析白客户和办事器之间的信息互换。


注意,客户--办事器之间可以使用SOCK_STREAM、SOCK_DGRAM 大概别的(只需它们接纳相反的)。一些很好的客户--办事器的例子有 telnet/telnetd、 ftp/ftpd 和 bootp/bootpd。每次你使用 ftp 的时分,在远 端都有一个 ftpd 为你办事。


寻常,在办事端仅有一个办事器,它接纳 fork() 来处理多个客户的连 接。基本的步骤是:办事器等候一个毗连,承受 (accept()) 毗连,然后 fork() 一个子历程处理它。这是下一章我们的例子中会讲到的。


19)简便的办事器


这个办事器所做的全部事情是在流式毗连上发送字符串 "Hello, World!\n"。你要测试这个步骤的话,可以在一台机器上运转该步骤,然后 在别的一机器上登岸:


$ telnet remotehostname 3490


remotehostname 是该步骤运转的机器的名字。


办事器代码:


#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #define MYPORT 3490 /*界说用户毗连端口*/ #define BACKLOG 10 /*几多等候毗连控制*/ main() {   int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */   struct sockaddr_in my_addr; /* my address information */   struct sockaddr_in their_addr; /* connector's address information */   int sin_size;   if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {   perror("socket");   exit(1);   }   my_addr.sin_family = AF_INET; /* host byte order */   my_addr.sin_port = htons(MYPORT); /* short, network byte order */   my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */   bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */   if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))== -1) {   perror("bind");   exit(1);   }   if (listen(sockfd, BACKLOG) == -1) {   perror("listen");   exit(1);   }   while(1) { /* main accept() loop */   sin_size = sizeof(struct sockaddr_in);   if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) {   perror("accept");   continue;   }   printf("server: got connection from %s\n", \   inet_ntoa(their_addr.sin_addr));   if (!fork()) { /* this is the child process */   if (send(new_fd, "Hello, world!\n", 14, 0) == -1)   perror("send");   close(new_fd);   exit(0);   }   close(new_fd); /* parent doesn't need this */   while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */ } }



假如你很挑剔的话,一定不满意我一切的代码都在一个很大的main() 函数中。假如你不喜好,可以区分得更细点。


你也可以用我们下一章中的步骤取得办事器端发送的字符串。


20)简便的客户步骤


这个步骤比办事器还简便。这个步骤的一切事情是经过 3490 端口毗连到下令行中指定的主机,然后取得办事器发送的字符串。


客户代码:


#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #define PORT 3490 /* 客户机毗连长程主机的端口 */ #define MAXDATASIZE 100 /* 每次可以吸收的最大字节 */ int main(int argc, char *argv[]) { int sockfd, numbytes; char buf[MAXDATASIZE]; struct hostent *he; struct sockaddr_in their_addr; /* connector's address information */ if (argc != 2) { fprintf(stderr,"usage: client hostname\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */ herror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; /* host byte order */ their_addr.sin_port = htons(PORT); /* short, network byte order */ their_addr.sin_addr = *((struct in_addr *)he->h_addr); bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */ if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) { perror("recv"); exit(1); } buf[numbytes] = '\0'; printf("Received: %s",buf); close(sockfd); return 0; }



注意,假如你在运转办事器之前运转客户步骤,connect() 将前往 "Connection refused" 信息,这十分有效。


21)数据包 Sockets


我不想讲更多了,以是我给出代码 talker.c 和 listener.c。


listener 在机器上等候在端口 4590 来的数据包。talker 发送数据包到 一定的机器,它包含用户本人令行输入的内容。


这里就是 listener.c:


#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #define MYPORT 4950 /* the port users will be sending to */ #define MAXBUFLEN 100 main() { int sockfd; struct sockaddr_in my_addr; /* my address information */ struct sockaddr_in their_addr; /* connector's address information */ int addr_len, numbytes; char buf[MAXBUFLEN]; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } my_addr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */ bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */ if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } addr_len = sizeof(struct sockaddr); if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, \ (struct sockaddr *)&their_addr, &addr_len)) == -1) { perror("recvfrom"); exit(1); } printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr)); printf("packet is %d bytes long\n",numbytes); buf[numbytes] = '\0'; printf("packet contains \"%s\"\n",buf); close(sockfd); }



注意在我们的调用 socket(),我们最初使用了 SOCK_DGRAM。同时, 没有必要去使用 listen() 大概 accept()。我们在使用无毗连的数据报套接 字!


底下是 talker.c:


#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #define MYPORT 4950 /* the port users will be sending to */ int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in their_addr; /* connector's address information */ struct hostent *he; int numbytes; if (argc != 3) { fprintf(stderr,"usage: talker hostname message\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */ herror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; /* host byte order */ their_addr.sin_port = htons(MYPORT); /* short, network byte order */ their_addr.sin_addr = *((struct in_addr *)he->h_addr); bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */ if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, \ (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) { perror("sendto"); exit(1); } printf("sent %d bytes to %s\n",numbytes,inet_ntoa(their_addr.sin_addr)); close(sockfd); return 0; }



这就是一切的了。在一台机器上运转 listener,然后在别的一台机器上 运转 talker。察看它们的通讯!
除了一些我在外表提到的数据套接字毗连的小细节外,关于数据套接 字,我还得说一些,当一个发言者召唤connect()函数时并指定承受者的地 址时,从这点可以看出,发言者只能向connect()函数指定的地点发送和接 受信息。因此,你不必要使用sendto()和recvfrom(),你完全可以用send() 和recv()代替。


22)壅闭


壅闭,你约莫早就听说了。"壅闭"是 "sleep" 的科技行话。你约莫注意 到前方运转的 listener 步骤,它在那边不休地运转,等候数据包的到来。 实践在运转的是它调用 recvfrom(),然后没多数据,因此 recvfrom() 说" 壅闭 (block)",直到数据的到来。


很多函数都使用壅闭。accept() 壅闭,一切的 recv*() 函数壅闭。它 们之以是能如此做是由于它们被允许如此做。当你第一次调用 socket() 建 立套接字形貌符的时分,内核就将它设置为壅闭。假如你不想套接字壅闭, 你就要调用函数 fcntl():


#include <unistd.h>


#include <fontl.h>


……


sockfd = socket(AF_INET, SOCK_STREAM, 0);


fcntl(sockfd, F_SETFL, O_NONBLOCK);


……


过设置套接字为非壅闭,你可以好效地"扣问"套接字以取得信息。如 果你实验着从一个非壅闭的套接字读信息并且没有任何数据,它不允许阻 塞--它将前往 -1 并将 errno 设置为 EWOULDBLOCK。


但是寻常说来,这种扣问不是个好想法。假如你让你的步骤在忙等状 态查询套接字的数据,你将糜费多量的 CPU 时间。更好的处理之道是用 下一章讲的 select() 去查询对否多数据要读过来。


23)select()--多路同步 I/O


固然这个函数有点奇异,但是它很有效。假定如此的情况:你是个服 务器,你一边在不休地从毗连上读数据,一边在侦听毗连上的信息。 没成绩,你约莫会说,不就是一个 accept() 和两个 recv() 吗? 这么 容易吗,伙伴? 假如你在调用 accept() 的时分壅闭呢? 你怎样可以同时接 受 recv() 数据? “用非壅闭的套接字啊!” 不可!你不想耗尽一切的 CPU 吧? 那么,该怎样是好?


select() 让你可以同时监督多个套接字。假如你想晓得的话,那么它就 会报告你哪个套接字准备读,哪个又准备写,哪个套接字又产生了例外 (exception)。


闲话少说,底下是 select():


#include <sys/time.h>


#include <sys/types.h>


#include <unistd.h>


int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);


这个函数监督一系列文件形貌符,特别是 readfds、writefds 和 exceptfds。假如你想晓得你对否可以从标准输入和套接字形貌符 sockfd 读入数据,你只需将文件形貌符 0 和 sockfd 到场到聚集 readfds 中。参 数 numfds 应该即是最高的文件形貌符的值加1。在这个例子中,你应该 设置该值为 sockfd+1。由于它一定大于标准输入的文件形貌符 (0)。 当函数 select() 前往的时分,readfds 的值修正为反应你选择的哪个 文件形貌符可以读。你可以用底下讲到的宏 FD_ISSET() 来测试。 在我们持续下去之前,让我来讲讲怎样对这些聚集举行利用。每个集 合典范都是 fd_set。底下有一些宏来对这个典范举行利用:


FD_ZERO(fd_set *set) – 扫除一个文件形貌符聚集


FD_SET(int fd, fd_set *set) - 添加fd到聚集


FD_CLR(int fd, fd_set *set) – 从聚集中移去fd


FD_ISSET(int fd, fd_set *set) – 测试fd对否在聚集中


最初,是有点乖僻的数据布局 struct timeval。偶尔你可不想永久等候 他人发送数据过去。约莫什么事变都没有产生的时分你也想每隔96秒在终 端上打印字符串 "Still Going..."。这个数据布局允许你设定一个时间,假如 时间到了,而 select() 还没有找到一个准备好的文件形貌符,它将前往让 你持续处理。


数据布局 struct timeval 是如此的:


struct timeval {


int tv_sec; /* seconds */


int tv_usec; /* microseconds */


};


只需将 tv_sec 设置为你要等候的秒数,将 tv_usec 设置为你要等候 的微秒数就可以了。是的,是微秒而不是毫秒。1,000微秒即是1毫秒,1,000 毫秒即是1秒。也就是说,1秒即是1,000,000微秒。为什么用标记 "usec" 呢? 字母 "u" 很象希腊字母 Mu,而 Mu 表现 "微" 的意思。固然,函数 前往的时分 timeout 约莫是剩余的时间,之以是是约莫,是由于它依托于 你的 Unix 利用体系。


哈!我们如今有一个微秒级的定时器!别盘算了,标准的 Unix 体系 的时间片是100毫秒,以是无论你怎样设置你的数据布局 struct timeval, 你都要等候那么长的时间。


另有一些幽默的事变:假如你设置数据布局 struct timeval 中的数据为 0,select() 将立刻超时,如此就可以好效地轮询聚集中的一切的文件形貌 符。假如你将参数 timeout 赋值为 NULL,那么将永久不会产生超时,即 不休比及第一个文件形貌符停当。最初,假如你不是很体贴等候多长时间, 那么就把它赋为 NULL 吧。


底下的代码演示了在标准输入上等候 2.5 秒:


#include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define STDIN 0 /* file descriptor for standard input */ main() { struct timeval tv; fd_set readfds; tv.tv_sec = 2; tv.tv_usec = 500000; FD_ZERO(&readfds); FD_SET(STDIN, &readfds); /* don't care about writefds and exceptfds: */ select(STDIN+1, &readfds, NULL, NULL, &tv); if (FD_ISSET(STDIN, &readfds)) printf("A key was pressed!\n"); else printf("Timed out.\n"); }



假如你是在一个 line buffered 终端上,那么你敲的键应该是回车 (RETURN),不然无论怎样它都市超时。
如今,你约莫回以为这就是在数据报套接字上等候数据的办法--你是对 的:它约莫是。有些 Unix 体系可以按这种办法,而别的一些则不克不及。你 在实验从前约莫要先看看本体系的 man page 了。


最初一件关于 select() 的事变:假如你有一个正在侦听 (listen()) 的套 接字,你可以经过将该套接字的文件形貌符到场到 readfds 聚集中来看是 否有新的毗连。


这就是我关于函数select() 要讲的一切的东西。



写在最初:学编程,但是每一局部都有本人的选择,每一种编程言语的存在都有其使用的朝向,选择你想从事的朝向,去举行切合的选择就对了!关于准备学习编程的小伙伴,假如你想更好的提升你的编程中心才能(内功)无碍从如今开头!

编程学习册老实享:

编程学习视频分享:

整理分享(多年学习的源码、项目实战视频、项目条记,基本入门教程)

接待转行和学习编程的伙伴,使用更多的材料学习发展比本人揣摩更快哦!

关于C/C++感兴致可以眷注小编在背景私信我:【编程交换】一同来学习哦!可以提取一些C/C++的项目学习视频材料哦!以前设置好了紧张词主动回复,主动提取就好了!

版权声明:本文来自互联网整理发布,如有侵权,联系删除

原文链接:https://www.yigezhs.comhttps://www.yigezhs.com/wangluozixun/40505.html


Copyright © 2021-2022 All Rights Reserved 备案编号:闽ICP备2023009674号 网站地图 联系:dhh0407@outlook.com