0%

Linux网络编程基础API

网络编程基础(2):Linux网络编程基础API

包含socket地址api,创建服务器&客户端必要的api,数据读写api,socket设置,信息获取api等
注意:这一章节非常重要,涉及很多基础操作,要多看!!!

主要内容

  • socket地址相关的API。IP地址+端口号
  • socket基础API。主要包括:创建socket,命名socket,监听,接收连接,设置socket选项等
  • 网络信息API。用于实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。

socket地址API

字节序问题

大端字节序:一个整数的高位字节(23-31bit)存储在内存的低地址处,低位字节(0-7bit)存储在内存的高地址处。

小端字节序:整数的高位字节存储在内存高地址处,低位字节存储在内存低地址处

现代PC大多采用小端序,所以也称小端序为主机字节序。网络中默认大端序,所以也称大端序为网络字节序。

Linux提供了四个函数来完成主机序和网络序之间的转换。

1
2
3
4
5
6
7
8
#include <netinet/in.h>
unsigned long int htonl( unsigned long int hostlong);
unsigned short int htons( unsigned short int hostshort);
//host --> net

unsigned long int ntohl( unsigned long int netlong);
unsigned short int ntohs( unsigned short int netlong);
//net --> host

通用socket地址

原始版

1
2
3
4
5
6
7
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family; //协议族信息
char sa_data[14]; //用于存放socket地址值。
}
//不同的协议族的地址信息具有不同的含义和长度。

改良后通用socket地址

1
2
3
4
5
6
7
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family; //协议族信息
unsigned long int __ss_align;
char __ss_padding[128-sizeof(__ss_align)]; //用于存放socket地址值。
}

专用的socket地址

上面的是考虑通用性,但是在实际程序编写中,常常根据具体选择的协议族来选用相应的专用socket地址。

协议族&地址族的类型

1
2
3
4
5
 协议族              地址族                    描述
|| || ||
PF_UNIX AF_UNIX UNIX本地域协议族
PF_INET AF_INET TCP/IPv4协议族
PF_INET6 AF_INET6 TCP/IPv6协议族

因为二者具有完全相同的值,所以PF_*与AF_*二者通常混用。

  • UNIX本地协议族专用socket地址

    1
    2
    3
    4
    5
    6
    #include <sys/un.h>
    struct sockaddr_un
    {
    sa_family_t sin_family; //地址族:AF_UNIX
    char sun_path[108]; //文件路径名
    }
  • TCP/IPv4专用socket地址(最为重要!!!)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <sys/un.h>
    struct sockaddr_in
    {
    sa_family_t sin_family; //地址族:AF_INET
    u_int16_t sin_port; //端口号,要用网络字节序表示
    struct in_addr sin_addr; //IPv4地址结构体
    };

    struct in_addr
    {
    u_int32_t s_addr; //IPv4地址,要用网络字节序
    };
  • TCP/IPv6专用socket地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <sys/un.h>
    struct sockaddr_in6
    {
    sa_family_t sin6_family; //地址族:AF_INET6
    u_int16_t sin6_port; //端口号,要用网络字节序表示
    struct in6_addr sin6_addr; //IPv6地址结构体
    u_int32_t sin6_flowinfo; //流消息,应设置为0
    u_int32_t sin6_scope_id; //scope ID,尚处于实验阶段
    };

    struct in6_addr
    {
    u_int32_t s_addr; //IPv6地址,要用网络字节序
    };

IP地址转换函数

通常人们喜欢用字符串形式下点分十进制来描述IP地址。但编程中需要先将它们转换成整数(二进制)才能使用。为此设计有如下的函数来解决相关问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//inet_addr函数
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
/*将点分十进制字符串表示的IPv4地址转为网络字节序下整数。
失败时返回INADDR_NONE*/

//inet_aton函数,完成inet_addr同等功能,形式略有不同
int inet_aton(const char* cp, struct in_addr* inp);
/*
将cp转换后得到的网络字节序存储在 inp中
成功时返回1,失败时返回0
*/

//inet_ntoa函数,完成相反功能,且具有不可重入性。
char* inet_ntoa(struct in_addr in);

所谓不可重入性,是因为该函数指向一个静态变量,意味着后面的将覆盖前面的。

代码示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gao@gao-VirtualBox:~/桌面/net_code$ cat in_ntoa.c
#include <stdio.h>
#include <arpa/inet.h>

int main(){

struct in_addr addr1; struct in_addr addr2;
inet_aton("1.2.3.4", &addr1);
inet_aton("10.194.71.60", &addr2);

char* szValue1 = inet_ntoa(addr1);
char* szValue2 = inet_ntoa(addr2);
printf("address1:%s\n", szValue1);
printf("address2:%s\n", szValue2);
return 0;
}
gao@gao-VirtualBox:~/桌面/net_code$ gcc in_ntoa.c -o test
gao@gao-VirtualBox:~/桌面/net_code$ ./test
address1:10.194.71.60
address2:10.194.71.60
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gao@gao-VirtualBox:~/桌面/net_code$ cat in_ntoa.c
#include <stdio.h>
#include <arpa/inet.h>

int main(){

struct in_addr addr1; struct in_addr addr2;

inet_aton("1.2.3.4", &addr1);
char* szValue1 = inet_ntoa(addr1);
printf("address1:%s\n", szValue1);

inet_aton("10.194.71.60", &addr2);
char* szValue2 = inet_ntoa(addr2);
printf("address2:%s\n", szValue2);
return 0;
}
gao@gao-VirtualBox:~/桌面/net_code$ gcc in_ntoa.c -o test
gao@gao-VirtualBox:~/桌面/net_code$ ./test
address1:1.2.3.4
address2:10.194.71.60
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_pton(int af, const char* src, void* dst);
/*
点分十进制-->网络字节序整数
af:指定地址协议族,可以是AF_INET,也可以是AF_INET6
src:字符串表示的点分十进制IP地址
dst:网络字节序整数表示的IP地址
成功时返回1,失败时返回0
*/

const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
/*
网络字节序-->点分十进制
前面的参数可类比,最后一个参数。cnt:指定目标存储单元大小。采用固定值
成功时返回得到的存储地址值,失败时返回NULL
*/

#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

socket基础API

创建socket

函数体

1
2
3
4
5
6
7
8
9
10
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
/*
domain:指定自己的底层协议:PF_INET(IPv4), PF_INET6(IPv6), PF_UNIX(本地)
type:SOCK_STREAM服务(基于流/TCP),SOCK_DGRAM(基于数据包/UDP)
protocol:前两个确认时,最后一个往往不需要特殊指定,通常设置为0,默认参数
*/

//成功时返回一个socket文件描述符,失败时返回-1,并设置errno

命名socket

函数体

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

bind(hServSock,(SOCKADDR*) &servAdddr, sizeof(servAdddr))

/*
socked:socket文件描述符
my_addr:socket地址
socket地址长度
*/

//成功时返回0,失败时返回-1,并设置errno。

监听socket

函数体

1
2
3
4
5
6
7
8
#include <sys/socket.h>
int listen(int sockfd, int backlog);
/*
sockfd:指定被监听的socket
backlog:提示内核监听的最大长度。监听队列长度如果超过backlog,服务器将不再受理新的客户连接
*/

//成功时返回0,失败时返回-1,并设置errno

设置backlog之后,最大连接数一般是backlog+1.示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

static bool stop = false;

static void handle_term(int sig)
{
stop = true;
}

int main(int argc, char* argv[])
{
signal(SIGTERM, handle_term);

if( argc <= 3)
{
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int backlog = atoi(argv[3]);

int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);

ret = listen(sock, backlog);
assert(ret != -1);

while( !stop )
{
sleep(1);
}

close(sock);
return 0;
}

结果

1
2
3
4
5
6
7
8
gao@gao-VirtualBox:~/桌面/net_code$ ./a 127.0.0.1 12345 5
#运行服务器端

#多次运行客户端连接
gao@gao-VirtualBox:~/桌面/net_code$ telnet 127.0.0.1 12345
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#查看网络连接状态
gao@gao-VirtualBox:~/桌面/net_code$ netstat -nt | grep 12345
tcp 0 0 127.0.0.1:48852 127.0.0.1:12345 ESTABLISHED
tcp 0 0 127.0.0.1:48882 127.0.0.1:12345 ESTABLISHED
tcp 0 0 127.0.0.1:12345 127.0.0.1:48852 ESTABLISHED
tcp 0 0 127.0.0.1:12345 127.0.0.1:48884 ESTABLISHED
tcp 0 0 127.0.0.1:12345 127.0.0.1:48882 ESTABLISHED
tcp 0 1 127.0.0.1:48888 127.0.0.1:12345 SYN_SENT
tcp 0 0 127.0.0.1:48878 127.0.0.1:12345 ESTABLISHED
tcp 0 0 127.0.0.1:48874 127.0.0.1:12345 ESTABLISHED
tcp 0 0 127.0.0.1:12345 127.0.0.1:48878 ESTABLISHED
tcp 0 1 127.0.0.1:48886 127.0.0.1:12345 SYN_SENT
tcp 0 0 127.0.0.1:48884 127.0.0.1:12345 ESTABLISHED
tcp 0 0 127.0.0.1:12345 127.0.0.1:48874 ESTABLISHED
tcp 0 0 127.0.0.1:12345 127.0.0.1:48876 ESTABLISHED
tcp 0 0 127.0.0.1:48876 127.0.0.1:12345 ESTABLISHED

#尝试连接了8个终端,前六个都到达了ESTABLISHED状态,之后的便不能建立连接了。

接受连接

函数体

1
2
3
4
5
6
7
8
9
10
11
#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr* addr. socklen_t *addrlen);
/*
sockfd是执行过listen调用的监听socket
addr是用来存储被接收连接的远端socket地址
addrlen是存储socket地址的长度
*/

//成功的时候返回一个新的连接socket,失败时返回-1,并设置errno。

测试功能:accept如何处理异常连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>

static bool stop = false;

static void handle_term(int sig)
{
stop = true;
}

int main(int argc, char* argv[])
{
signal(SIGTERM, handle_term);

if( argc <= 3)
{
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int backlog = atoi(argv[3]);

int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);

ret = listen(sock, backlog);
assert(ret != -1);

sleep(30);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);

int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd < 0)
{
printf("errno is: %d\n", errno);
}
else
{
char remote[INET_ADDRSTRLEN];
printf("connected with ip: %s and port: %d\n", inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));
close(connfd);
}

close(sock);
return 0;
}

执行这个服务器程序,同时启动telnet连接到服务器并很快的断开。即在listen和accept之间,强行终止telnet客户端。观察accept的处理

1
2
3
4
gao@gao-VirtualBox:~/桌面/net_code$ ./accept_pro 127.0.0.1 54321 5
connected with ip: 127.0.0.1 and port: 50722
gao@gao-VirtualBox:~/桌面/net_code$ netstat -nt | grep 54321
tcp 0 0 127.0.0.1:50722 127.0.0.1:54321 TIME_WAIT

accept并不会检测是否异常,它只是从监听队列中取出连接。

发起连接

服务器端通过listen调用来被动接受连接,客户端通过connect来主动建立连接

函数体

1
2
3
4
5
6
7
8
9
10
11
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

/*
sockfd:由客户端创建的socket文件描述符
serv_addr:服务器监听的socket地址
addrlen:地址的长度
*/

//connect成功时返回0,失败时返回-1并设置errno

关闭连接

函数体

1
2
3
4
5
6
#include <unistd.h>
int close(int fd);
/*
注意点:close并非直接关闭一个连接,而是将fd的引用计数减一。只有当fd的引用计数为0时,才真正关闭连接。
在多进程程序中,一次fork,会默认将父进程中打开的socket的引用计数加1.因此父进程和子进程都需要close关闭
*/
1
2
3
4
5
6
7
8
#include <sys/socket.h>
int shutdown(int sockfd, int howto);

/*
SHUT_RD:关闭sockfd上读的操作接口
SHUT_WR:关闭sockfd上写的操作接口
SHUT_RDWR:关闭sockfd上读和写的操作接口
*/

数据读写API

针对文件的I/O函数,read,write也都是可以的。但是socket编程提供了几个专门用于socket数据读写的函数,可以加强对数据读取的控制

TCP数据读写

函数体

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>
#include <sys/socket.h>

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

/*
recv读取sockfd上的数据,buf&len分别指定读缓冲区的位置和大小,flags参数通常置0
成功时返回实际读取到的数据的长度。返回0,意味着对方关闭连接。出错时返回-1
send向sockfd上写入数据,buf&len参数分别指定写缓冲区的位置和大小。
成功时返回实际读取到的数据的长度。出错时返回-1
*/

UDP数据读写

函数体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags,
struct aockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, void* buf, size_t len, int flags,
struct aockaddr* dest_addr, socklen_t* addrlen);

/*
前面的参数一致,因为UDP并没有建立连接,所以
读取时要获取发送端socket地址&该地址的长度
写入数据时,指定接收端的socket地址&地址长度
*/

//注意:也可用于TCP连接下数据读写,将最后两个参数置为NULL即可

地址信息函数

函数体

1
2
3
4
5
6
7
8
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);

/*
getsockname函数获得sockfd对应的本端socket地址,存储在address中。
getpeername函数获得sockfd对应的远端socket地址,存储在address中。
*/

socket选项

函数体

1
2
3
4
5
6
7
8
9
10
11
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int option_name, void* option_value,socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void* option_value,socklen_t option_len);

/*
sockfd参数指定被操作的目标
level参数指定要操作哪个协议
option_name指定选项的名字
option_value被操作选项的值
option_len被操作选项的长度
*/

选项列表

两个相对重要的点:SO_REUSEADDR(重用本地地址) TCP_NODELAY(禁止Nagle算法)

SO_REUSEADDR

某些场合下需要尽快的解决TIME_WAIT的影响,快速启动。可更改设置

1
2
3
4
5
6
7
8
9
//SO_REUSEADDR 
#define TRUE 1
#define false 0
int option; optlen = sizeof(option); option = TRUE; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void*)& option, optlen);
//将该属性改为true即可!
/*
当该选项为false(0)时,Time-wait下端口号属于被占用状态,
但是当该选项置1(为真)时,Time-wait下端口号可以被重新分配给新的套接字
*/

TCP套接字默认使用Nagel算法交换数据

采用该算法的好处为:减少网络负载和混乱程度,最大程度地利用缓冲

其弊端为:会牺牲一定的传输速度。

使用场景:当传输大文件数据时,不开启Nagel算法,依然可以最大程度利用缓存,且传输速度高。

默认状态下TCP_NODELAY选项为0,修改为1后,便禁用了Nagel算法。

1
2
3
4
//TCP_NODELAY修改Nagel算法状态 
int opt_val = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, sizeof(opt_val));
//代码执行后,该算法被禁用

剩余的若干更改可参考情况具体应用

网络信息API

四个重点函数

1
2
3
4
5
6
7
8
9
10
11
//获取主机信息
#include <netdb.h>
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);

/*
name:目标主机主机名
addr:目标主机的IP地址
len:addr所指IP地址的长度
type:addr所指IP地址的类型,包括AF_INET, AF_INET6
*/

结构体hostent的定义

1
2
3
4
5
6
7
8
9
#include <netdb.h>
struct hostent
{
char* h_name; //主机名称
char** h_aliases; //主机别名列表
int h_addrtype; //主机地址类型
int h_length; //主机地址长度
char** h_addr_list; //按网络字节序列出的主机IP地址列表
};
1
2
3
4
5
6
7
8
9
10
//获取服务信息
#include<netdb.h>
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);

/*
name:目标服务的名字
proto:传递tcp/udp/NULL。传NULL意味着获取所有类型的服务
port:目标服务对应的端口号
*/

结构体servent的定义

1
2
3
4
5
6
7
8
#include <netdb.h>
struct servent
{
char* s_name; //服务名称
char** s_aliases; //服务的别名列表
int s_port; //端口号
char* s_proto; //服务类型,通常是tcp或者udp
};

参考内容

游双老师《Linux高性能服务器编程》
尹圣雨老师《TCP/IP网络编程》
另:完全零基础建议先看尹圣雨老师的书,更为通俗易懂,我也是先用这本书入的门。