网络编程基础——常见 API 总结

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程基础——常见 API 总结相关的知识,希望对你有一定的参考价值。

参考技术A

这个 API 常用来做 DNS 查询,用法如下:

返回一个 status 值,如果 status == 0 则失败,这里有一个详细的例子,转自( http://beej.us/guide/bgnet/examples/showip.c ):

最后用 freeaddrinfo() free 掉内存

下面我们正式讲述与 socket() 相关的 API

作用:用 socket() 拿到 socket 描述符

用法如下:

每个参数的意义如下:

domain 的值可能是 PF_INET(ipv4) 或者 PF_INET6(ipv6),我们输出一下这个值:

其实 AF_INET 和 PF_INET 是一样的东西。

我们要做的其实是: 在 struct sockaddr_in 中使用 AF_INET,在调用 socket() 中使用 PF_INET

结合 getaddrinfo() 我们通常这样使用:

socket() 只返回一个 socket 描述符用于之后的系统调用,或者 -1 表示错误

作用:bind ip 和 port

bind() 函数如下:

参数意义如下:

sockfd 文件描述符,my_addr ip 地址以及端口,addrlen 地址长度,区分 ipv4,ipv6

来让我们看一个例子

这里使用了 AI_PASSIVE 作用是:告诉程序去绑一个正在运行的主机 ip,各位可以试着去输出一下,很奇怪的 ip。如果你想绑定一个特定的本地 ip。那么就不要使用 AI_PASSIVE 并给 getaddrinfo() 第一个参数提供 ip

如果 bind() 错了,也会返回 -1

在客户端的时候(不用关心绑定的端口号)不需要使用 bind(),直接调用 connect() 函数就好,它会检查 socket 有没有绑定,如果没有 bind 就会自己 bind 到一个没有用过的端口号上

作用:建立连接

用法如下:

从参数名就可以知道每个参数的含义

而且为了连接服务器,只需要 getaddrinfo() 提供的 res。比如:

connect 也会返回一个 errno,如果为 -1 就说明没连接上。

作用:监听端口

用法如下:

这里出现了一个我们从未接触的参数 backlog 简单来说, backlog 参数指定队列将保留的挂起连接数

大多数系统默默地将此数量限制在 20 左右;你可以把它设置为 5 或 10。同样它也会返回 -1 表示错误。而且,我们要在 bind() 后才能使用 listen(),只有这样才能控制 listen 的端口

大概这样使用

作用:在 listen 中我们说到有一个等待连接的队列,队列中的每一个连接都等待着唤醒。accept() 用来唤醒队列中的连接。

用法如下:

accept() 会返回一个新的 socket 描述符,现在你有两个 socket 描述符,原来那个依旧在监听连接。新的这个描述符已经准备使用 send() 和 recv() 了。

我们注意一下第二个参数,通常是 sockaddr_storage (与 sockaddr 兼容),在错误发生时 accept() 同样也会返回 -1

大概这样使用:

作用:接发数据

用法如下:

在 send() 函数中返回值是发送的字节长度,很可能比你提供的参数 len 要小,比如你要发送大数据的时候,它尽量去发最大数据,但还是可能没有那么长于是会丢掉多的数据。要记住! 如果返回的长度小于你提供的参数 len,那么是你去决定是否要补全剩下的数据

返回 -1,代表着错误。

recv() 函数差不多,如果发生错误,也会返回 -1。但是也可能返回 0。原因是:远程机器关闭了与你的连接。

作用:UDP 的 send() 和 recvfrom()

用法如下:

由于 UDP 不需要连接,所以需要手动的填上参数。

作用:关闭连接

用法如下:

close() 就是最简单的关闭文件的方式,所以同样可以用在 socket 上。而且一旦某个 socket 被 close 了,试图在远端读或写这个 socket 都会收到 error

而 shutdown() 函数有那么一些不一样。它允许你给出关闭连接的方向 how

how 有三个值

值得注意的是: shutdown() 并不是真正关闭 socket 描述符,只是让它不能使用。如果要去除 socket 描述符的占用。你还是需要使用 close()

作用:得到对方的信息

用法如下:

获得地址(addr)后,可以使用 inet_ntop(),getnameinfo() 或 gethostbyaddr() 来打印或获取更多信息。但是,你不能得到他们的登录名。 但是如果对方程序正在跑一个 ident 守护进程,你还是有可能得到对方的登录名的

这个函数更简单了

作用:得到自己的信息

用法如下:

参数很简单:hostname 是一个指向字符数组的指针,它将包含函数返回的主机名,size 是 hostname 数组的字节长度。成功完成时函数返回 0,错误时返回 -1

前面写了那么多,现在用一张图来总结一下吧:

《Linux高性能服务器编程》学习总结——Linux网络编程基础API

第五章      Linux网络编程基础API

  对于网络编程,首先要了解的就是字节序的问题,字节序分为主机字节序和网络字节序,主机字节序又称小端字节序,是低字节存放在地地址,而网络字节序又称大端字节序,是低字节放在高地址。当数据在不同的机器上传播时,就需要统一字节顺序以保证不出现错误。在发送数据前,先将需要转变的数据转成网络字节序再发送,接收时先转成主机字节序再处理,要特别注意的是,即使是本机的两个进程通信,也要考虑字节序的问题,比如JAVA的虚拟机就使用大端字节序。使用如下代码可以查看本机的字节顺序:

 1 /*************************************************************************
 2     > File Name: 5-1.cpp
 3     > Author: Torrance_ZHANG
 4     > Mail: [email protected]
 5     > Created Time: Thu 01 Feb 2018 12:28:00 AM PST
 6  ************************************************************************/
 7 
 8 #include<iostream>
 9 #include<stdio.h>
10 using namespace std;
11 
12 void byteorder() {
13     union {
14         short value;
15         char union_bytes[sizeof(short)];
16     }test;
17     test.value = 0x0102;
18     if((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2)) {
19         printf("big endian\\n");
20     }
21     else if((test.union_bytes[1] == 1) && (test.union_bytes[0] == 2)) {
22         printf("little endian\\n");
23     }
24     else printf("unknown...\\n");
25 }
26 
27 int main() {
28     byteorder();
29 }

  在socket网络编程接口中,用来表示socket地址的是结构体sockaddr,但由于其无法容纳所有协议族的信息,所以又有了sockaddr_in、sockaddr_in6和sockaddr_un等专用socket地址结构,在编程使用中只需要将对应的地址结构填好再强转成sockaddr类型即可,这样做的好处就是可以简化socket接口,只需要设立一个通用接口即可提供给不同协议族使用。

  我们平常使用的IP地址是点分十进制形式的字符串,但是在网络连接中我们需要将其转换成对应的unsigned int类型的数才能使用,所以,API中为我们提供了几个函数:

1 #include<arpa/inet.h>
2 in_addr_t inet_addr(const char* strptr);
3 int inet_aton(const char* cp, struct in_addr* inp);
4 char* inet_ntoa(struct in_addr in);
5 inti net_pton(int af, const char* src, void* dst);
6 const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);

  其中前三个只适用于ipv4,而后两个适用于ipv4和ipv6。值得注意的是,inet_ntoa函数是不可重入的,其内部使用了一个静态变量来存储结果,函数返回的是静态内存。所以多次调用这个函数返回的是同一块内存,其多次的值都为最后一次的结果。

  socket的本质就是一个文件描述符,下面我们总结一下常用的socket函数:

 1 #include<sys/types.h>
 2 #include<sys/socket.h>
 3 int socket(int domain, int type, int protocol);
 4 int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
 5 int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
 6 int listen(int sockfd, int backlog);
 7 int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
 8 int close(int fd);
 9 int shutdown(int sockfd, int howto);
10 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
11 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
12 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
13 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);

  前三个函数较为简单,不再赘述。对于监听函数listen,第二个参数是backlog,表示内核监听队列的最大长度,如果数量超过了这个值,则服务器将不受理新的客户连接,下面我们用一个实验来测试一下这个最大长度和backlog有什么关系:

 1 /*************************************************************************
 2     > File Name: 5-3.cpp
 3     > Author: Torrance_ZHANG
 4     > Mail: [email protected]
 5     > Created Time: Thu 01 Feb 2018 02:06:39 AM PST
 6  ************************************************************************/
 7 
 8 #include<iostream>
 9 #include<stdio.h>
10 #include<sys/socket.h>
11 #include<netinet/in.h>
12 #include<arpa/inet.h>
13 #include<signal.h>
14 #include<unistd.h>
15 #include<stdlib.h>
16 #include<assert.h>
17 #include<string.h>
18 using namespace std;
19 
20 static bool stop = false;
21 static void handle_term(int sig) {
22     stop = true;
23 }
24 
25 int main(int argc, char **argv) {
26     signal(SIGTERM, handle_term);
27     if(argc <= 3) {
28         printf("usage: %s ip_address port_number backlog\\n", basename(argv[0]));
29         return 1;
30     }
31 
32     const char* ip = argv[1];
33     int port = atoi(argv[2]);
34     int backlog = atoi(argv[3]);
35 
36     int sock = socket(AF_INET, SOCK_STREAM, 0);
37     assert(sock >= 0);
38 
39     struct sockaddr_in address;
40     bzero(&address, sizeof(address));
41     address.sin_family = AF_INET;
42     address.sin_port = htons(port);
43     inet_pton(AF_INET, ip, &address.sin_addr);
44 
45     int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
46     assert(ret != -1);
47 
48     ret = listen(sock, backlog);
49     assert(ret != -1);
50 
51     while(!stop) {
52         sleep(1);
53     }
54     close(sock);
55     return 0;
56 }

  运行服务器,监听12345端口并设定最大监听队列为5。用telnet模拟客户端连接,发现当telnet运行到第7个时就连接不上,隔一段时间后返回连接超时,而此时使用netstat -nt | grep 12345查看,发现有6个已经建立的连接,第7个处于SYN_SENT状态。

技术分享图片

技术分享图片

  综上,我们设定的backlog值加1就是监听队列最大能监听的数量。

  对于接受连接的accept函数,它从监听队列中取出一个连接,与其建立连接,而不管其处于ESTABLISHED或CLOSE_WAIT状态,更不关心任何网络变化。

  当连接结束时,我们调用close将其关闭,但是close并不总是关闭连接,而是将这个文件描述符的引用计数减1,只有当这个文件描述符的引用计数为0时才会真正关闭。在多进程程序中,一次fork就会使得父进程中打开的文件描述符引用计数加1,所以这种情况下我们就应该对父子进程中的文件描述符都执行一次close。如果要立即终止连接,就可以使用下面的shutdown函数,参数howto的取值分别为SHUT_RD、SHUT_WR和SHUT_RDWR。

  接下来的函数是重头戏,分别是TCP和UDP的发送和接收数据函数,先看TCP的,最后一个参数flags提供了一些额外的控制,一般情况下取0即可,或者是若干个宏的逻辑或,常用的有MSG_OOB用来发送和接收紧急数据等。我们来通过一个服务器和客户端的例子来说明如何发送带外数据。

 1 /*************************************************************************
 2     > File Name: 5-6.cpp
 3     > Author: Torrance_ZHANG
 4     > Mail: [email protected]
 5     > Created Time: Thu 01 Feb 2018 04:37:10 AM PST
 6  ************************************************************************/
 7 
 8 #include"head.h"
 9 using namespace std;
10 
11 int main(int argc, char **argv) {
12     if(argc <= 2) {
13         printf("usage: %s ip_address port_number\\n", basename(argv[0]));
14         return 1;
15     }
16     const char* ip = argv[1];
17     int port = atoi(argv[2]);
18 
19     struct sockaddr_in server_address;
20     bzero(&server_address, sizeof(server_address));
21     server_address.sin_family = AF_INET;
22     inet_pton(AF_INET, ip, &server_address.sin_addr);
23     server_address.sin_port = htons(port);
24 
25     int sockfd = socket(AF_INET, SOCK_STREAM, 0);
26     assert(sockfd >= 0);
27     if(connect(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
28         printf("connection failed\\n");
29     }
30     else {
31         const char* oob_data = "abc";
32         const char* normal_data = "123";
33         send(sockfd, normal_data, strlen(normal_data), 0);
34         send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
35         send(sockfd, normal_data, strlen(normal_data), 0);
36     }
37     close(sockfd);
38     return 0;
39 }
 1 服务器端:
 2 /*************************************************************************
 3     > File Name: 5-7.cpp
 4     > Author: Torrance_ZHANG
 5     > Mail: [email protected]
 6     > Created Time: Thu 01 Feb 2018 04:44:00 AM PST
 7  ************************************************************************/
 8 
 9 #include"head.h"
10 using namespace std;
11 
12 #define BUF_SIZE 1024
13 
14 int main(int argc, char **argv) {
15     if(argc <= 2) {
16         printf("usage: %s ip_address port_number\\n", basename(argv[0]));
17         return 1;
18     }
19     const char* ip = argv[1];
20     int port = atoi(argv[2]);
21 
22     struct sockaddr_in address;
23     bzero(&address, sizeof(address));
24     address.sin_family = AF_INET;
25     inet_pton(AF_INET, ip, &address.sin_addr);
26     address.sin_port = htons(port);
27 
28     int sock = socket(AF_INET, SOCK_STREAM, 0);
29     assert(sock >= 0);
30 
31     int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
32     assert(ret != -1);
33 
34     ret = listen(sock, 5);
35     assert(ret != -1);
36 
37     struct sockaddr_in client;
38     socklen_t client_addrlength = sizeof(int);
39     int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
40     if(connfd < 0) {
41         printf("errno is: %d\\n", errno);
42     }
43     else {
44         char buffer[BUF_SIZE];
45 
46         memset(buffer, 0, sizeof(buffer));
47         ret = recv(connfd, buffer, BUF_SIZE - 1, 0);
48         printf("got %d bytes of normal data ‘%s‘\\n", ret, buffer);
49 
50         memset(buffer, 0, sizeof(buffer));
51         ret = recv(connfd, buffer, BUF_SIZE - 1, MSG_OOB);
52         printf("got %d bytes of oob data ‘%s‘\\n", ret, buffer);
53 
54         memset(buffer, 0, sizeof(buffer));
55         ret = recv(connfd, buffer, BUF_SIZE - 1, 0);
56         printf("got %d bytes of normal data ‘%s‘\\n", ret, buffer);
57 
58         close(connfd);
59     }
60     close(sock);
61     return 0;
62 }

技术分享图片

  运行结果如图,从这个实验中可以看出三点:首先,带外数据即紧急数据只能有1字节,因为紧急指针指向的位置是带外数据的下一个字节,而TCP首部并没有紧急数据长度的字段,所以只用紧急数据的最后一个字节作为了带外数据,其余还是普通数据;其次,我们看到第一次和第二次调用了send,但是接收的时候是一同接受了前两次中的普通数据,这就很好地说明了TCP是基于流的协议;最后,我们发现当整个数据报中存在紧急数据的时候,其余普通数据就会被紧急数据分隔开,不能一同读取。

  UDP的发送和接收函数与TCP的类似,区别在于多了两个参数用来表示对端的socket地址结构。而这两个函数也可用于面向连接时候,只需要把后两个参数设定为NULL即可。

  API中还定义了两个不太常用的通用数据读写系统调用recvmsg和sendmsg,还有sockatmark函数用来判断sockfd是否处于带外标记状态,用getsockname和getpeername函数获取本端和对端的socket地址结构,使用较为简单。

  socket选项信息是用来对socket的文件属性进行读取和设置的,其两个函数原型为:

1 #include<sys/socket.h>
2 int getsockopt(int sockfd, int level, int option_name, void *option_value, socklen_t* restrict option_len);
3 int setsockopt(int sockfd, int level, int option_name, const void *option_value, socklen_t* option_len);

  在参数中,level指定了要操作哪个协议的属性,option_name则指定选项的名字,这两项都有固定的搭配,接下来的option_value和option_len是值的大小和长度。值得注意的是,有些选项需要在TCP连接建立之前就设置好,即在调用listen和connect函数之前就设置,这是因为某些选项是TCP连接时需要互相协商的选项,而调用这两个函数时表示已经开始连接或完成连接。

  选项种类有很多,常用的有:SO_REUSEADDR选项用来重用TCP端口;SO_RCVBUF和SO_SNDBUF选项用来设置接收及发送缓冲区大小,SO_RCVLOWAT和SO_SNDLOWAT设置缓冲区的低水位标记,意思是如果缓冲区的可读数据或可写空间大于低水位标记系统才通知应用程序从缓冲区读出数据或写入数据,一般默认低水位标记为1字节;SO_LINGER选项用来控制close在关闭TCP时的行为。

  socket也提供了几个网络信息API,分别用来根据主机名和ip地址获取主机信息,根据名称或端口获取服务信息,其函数原型如下:

1 #include<netdb.h>
2 struct hostent* gethostbyname(const char* name);
3 struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
4 struct servent* getservbyname(const char* name, const char* proto);
5 struct servent* getservbyport(int port, const char* proto);

  需要指出的是,以上四个函数都是不可重入的,即非线程安全的,不过netdb.h头文件也给出了他们的可重入版本,就是在函数名后加上_r。

以上是关于网络编程基础——常见 API 总结的主要内容,如果未能解决你的问题,请参考以下文章

Java基础编程篇(1.Java语言概述)

C/C++编程基础算法总结

java进阶学习 --java网络编程一(转)

Java体系总结

Java学习路线

文件系统总结