Linux网络编程基础API学习笔记

Linux网络编程基础API-学习笔记

主机字节序和网络字节序

大端字节序:网络字节序

利用union公用体查看自己电脑的字节序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
void byteorder()
{
union
{
short value;
char union_bytes[ sizeof( short ) ];
} test;
test.value = 0x0102;
if ( ( test.union_bytes[ 0 ] == 1 ) && ( test.union_bytes[ 1 ] == 2 ) )
{
printf( "big endian\n" );
}
else if ( ( test.union_bytes[ 0 ] == 2 ) && ( test.union_bytes[ 1 ] == 1 ) )
{
printf( "little endian\n" );
}
else
{
printf( "unknown...\n" );
}
}

现代的PC机大多采用小端序:主机字节序

socket

linux:所有的东西都是文件。socket:可读,可写可控制,可关闭的文件描述符。

创建socket:

1
2
3
#include <sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);//domain:底层协议族 type:服务类型(流服务,数据报服务)
1
2
3
4
5
6
//创建socket
//协议底层族:PF_INET 用于IPV4 PF_INET6用于IPV6
//SOCK_STREAM,流服务,对于TCP/IP协议族,标识TCP协议
//SOCK_DGRAM 数据报服务,对于对于TCP/IP协议族,表示传输层使用UDP
//0表示默认协议
int sock = socket( PF_INET, SOCK_STREAM, 0 );

bind

将一个socket和socket地址绑定称为给socket命名。

1
2
3
#include <sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr * myaddrsocklen_taddrlen

绑定成功返回0,失败返回-1.

监听socket

创建一个监听队列存放待处理的客户连接

1
2
#include<sys/socket.h>
int listen(int sockfd,int backlog)

backlog:内核监听队列的最大长度,

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
#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>

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

int main( int argc, char* argv[] )
{
signal( SIGTERM, handle_term );
//该服务程序接收三个参数:IP地址,端口号,backlogzhi
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] );
//创建socket
//协议底层族:PF_INET 用于IPV4 PF_INET6用于IPV6
//SOCK_STREAM,流服务,对于TCP/IP协议族,标识TCP协议
//SOCK_DGRAM 数据报服务,对于对于TCP/IP协议族,表示传输层使用UDP
//0表示默认协议
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );//检查创建是否成功

struct sockaddr_in address;//用于存储 IPv4 地址和端口信息。
bzero( &address, sizeof( address ) );
/*使用 bzero 函数将 address 结构的内存区域清零,以确保结构中的所有字段都被初始化为零。bzero 是一个用于将内存区域清零的函数,但在一些系统中,可能会看到 memset 函数来执行相同的操作。*/
address.sin_family = AF_INET;//这行代码设置 address 结构的地址族为 AF_INET,表示使用 IPv4 地址。
inet_pton( AF_INET, ip, &address.sin_addr );//将字符串格式的 IP 地址转换为二进制形式并将结果存储在 address.sin_addr 中
//用 htons 函数将端口号从主机字节序转换为网络字节序。网络字节序是一种特定的字节序,通常是大端序(Big Endian)
address.sin_port = htons( port );//设置端口号并进行字节序转换
//执行了 bind 函数,用于将一个本地地址(例如 IP 地址和端口号)绑定到一个已创建的套接字 sock 上
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );//测试是否绑定成功
//监听,设置监听队列长度
ret = listen( sock, backlog );
assert( ret != -1 );
//循环等待连接,直到有SIGTREAM信号将它中断
while ( ! stop )
{
sleep( 1 );
}
//关闭socket
close( sock );
return 0;
}

运行程序:./a.out 172.21.245.193 255 5

报错解决:

1
2
: Cannot assign requested address
a.out: testlisten.cpp:48: int main(int, char**): Assertion `ret != -1' failed.
  1. 第一步,先查一下自己ubuntu的IP地址 hq123@ubuntu:~/a_test$ ifconfig server.sin_addr.s_addr = inet_addr("xxx"); // 服务器的 IP 地址
  2. 第二步 ,bind 绑定的IP地址要和自己电脑(ubuntu)的IP地址一

运行上述程序,在客户端远程连接服务器:

telnet 服务器公网IP 255

此时端口状态:

1
2
3
4
❯ lsof -i :255
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
a.out 179304 root 3u IPv4 1430112 0t0 TCP localhost:255 (LISTEN)

查看端口状态:netstat -nt |grep 255

1
2
❯ netstat -nt |grep 255
tcp 0 0 172.21.245.193:255 111.199.69.168:2733 ESTABLISHED

释放端口:sudo fuser -k -n tcp 255

通过实验可以发现,监听队列的长度超过backlog的时候服务器不受理新的客户连接

服务器开启防火墙端口

查看防火墙状态

1
systemctl status firewalld.service 

开启防火墙

1
systemctl start firewall.service

重启防火墙

1
firewall-cmd --reload

关闭防火墙

1
systemctl stop firewalld.service

开放端口(更改后要重启防火墙)

1
firewall-cmd --zone=public --add-port=255/tcp --permanent

接受连接

1
2
3
#include <sys/types.h>
#include<sys/socket.h>
int acept(int sockfd,struct sockaddr* addr,socklen_t *addrlen);

addr:被接受的远端socket地址,addrlen地址长度

接受一个异常的连接:

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
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

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

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 sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );

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

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

struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
//定义了一个变量 client_addrlength,用于存储客户端地址结构体的大小。
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
//使用 accept 函数接受客户端的连接请求。sock 是监听套接字,&client 是用于存储客户端地址信息的结构体。connfd 是返回的新的套接字描述符,用于与客户端进行通信。
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
char remote[INET_ADDRSTRLEN ];//定义一个字符数组 remote,用于存储远程客户端的 IP 地址。
printf( "connected with ip: %s and port: %d\n",
inet_ntop( AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN ), ntohs( client.sin_port ) );
//将二进制表示的 IPv4 地址转换为可读的字符串表示形式,并将结果存储在 remote 中
//将网络字节序的端口号转换为主机字节序。client.sin_port 存储了客户端的端口号。
close( connfd );
}

close( sock );//关闭连接,并非总是直接关闭连接而是将sock对应的引用计数减一
return 0;
}

address.sin_port 字段表示套接字绑定的端口号。具体来说,sin_portstruct sockaddr_in 结构体的一个字段,它存储了套接字的端口信息。和之前不一样,这次是直接连接了

客户端连接
服务器连接

accept对客户端网络的断开毫不知情。

TCP数据读写

对文件的读写同样适用于socket,其中recv读取sockfd上的数据,send向sockfd写数据

客户端:

send( sockfd, normal_data, strlen( normal_data ), 0 );

使用 send 函数,第一个参数是套接字描述符,第二个参数是指向要发送数据的指针,第三个参数是要发送的数据的长度,最后一个参数是标志,此处标志为 0,表示普通数据

send(sockfd, oob_data, strlen(oob_data), MSG_OOB);

发送带外数据到服务器,使用 send 函数,最后一个参数设置为 MSG_OOB,表示带外数据,用于发送或接受紧急数据

整体程序:

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
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

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

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

int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( sockfd >= 0 );
//connect 发起连接。用于建立与远程服务器的连接
if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
{
printf( "connection failed\n" );
}
else
{
printf( "send oob data out\n" );
const char* oob_data = "abc";
const char* normal_data = "123";

send( sockfd, normal_data, strlen( normal_data ), 0 );
send( sockfd, oob_data, strlen( oob_data ), MSG_OOB );
send( sockfd, normal_data, strlen( normal_data ), 0 );
}

close( sockfd );
return 0;
}

服务器接收:

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
67
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define BUF_SIZE 1024

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

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 sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );

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

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

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 buffer[ BUF_SIZE ];

memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
printf( "got %d bytes of normal data '%s'\n", ret, buffer );

memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );
printf( "got %d bytes of oob data '%s'\n", ret, buffer );

memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
printf( "got %d bytes of normal data '%s'\n", ret, buffer );

close( connfd );
}

close( sock );
return 0;
}

memset(buffer, '\0', BUF_SIZE);buffer 数组初始化为零,以确保不会出现垃圾数据。

ret = recv(connfd, buffer, BUF_SIZE - 1, 0); 使用 recv 函数接收普通数据,connfd 是连接的套接字描述符,buffer 是接收数据的缓冲区,BUF_SIZE - 1 是要接收的最大字节数,0 表示没有特殊标志。

客户端
服务端

UDP数据读写

和tcp不同,由于udp是无连接的,因此因此每次都要指定发送端的socket地址(ip+端口)

重用本地地址

  • SO_REUSEADDR 是套接字选项的一种,用于控制套接字地址的重用。
  • SO_REUSEADDR 设置为非零值时,表示允许重用处于 TIME_WAIT 状态的套接字地址。
  • 这对于快速重新启动服务器程序而无需等待 TIME_WAIT 超时非常有用。
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
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

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

int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int reuse = 1;
setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );

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, 5 );
assert( ret != -1 );

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;
}

修改TCP缓冲区

客户端修改发送缓冲区:

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 <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define BUFFER_SIZE 512

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

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

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

int sendbuf = atoi( argv[3] );
int len = sizeof( sendbuf );
setsockopt( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof( sendbuf ) );
/*
SOL_SOCKET 表示套接字层级的选项,用于指定要设置或获取的选项层级。
这是一个通用的套接字选项,可用于在套接字 API 中设置或获取各种配置。
SO_SNDBUF 表示设置或获取套接字发送缓冲区大小的选项
这行代码使用 setsockopt 函数设置了套接字 sock 的发送缓冲区大小,sendbuf 是要设置的缓冲区大小。这意味着在后续的数据发送过程中,套接字将使用这个指定大小的发送缓冲区。
*/
getsockopt( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, ( socklen_t* )&len );
/*
这行代码使用 getsockopt 函数获取套接字 sock 的当前发送缓冲区大小,并将结果存储在 sendbuf 中。然后,打印出当前的发送缓 冲区大小。
*/
printf( "the tcp send buffer size after setting is %d\n", sendbuf );

if ( connect( sock, ( struct sockaddr* )&server_address, sizeof( server_address ) ) != -1 )
{
char buffer[ BUFFER_SIZE ];
memset( buffer, 'a', BUFFER_SIZE );
send( sock, buffer, BUFFER_SIZE, 0 );
}

close( sock );
return 0;
}
socket选项

服务器端

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
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define BUFFER_SIZE 1024

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

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 sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int recvbuf = atoi( argv[3] );
int len = sizeof( recvbuf );
setsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof( recvbuf ) );
getsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, ( socklen_t* )&len );
printf( "the receive buffer size after settting is %d\n", recvbuf );

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

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

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 buffer[ BUFFER_SIZE ];
memset( buffer, '\0', BUFFER_SIZE );
while( recv( connfd, buffer, BUFFER_SIZE-1, 0 ) > 0 ){}
close( connfd );
}

close( sock );
return 0;
}

tips

注意有些socket修改选项(例如本次的修改缓冲区选项),在客户端必须要在connect前修改好,connect调用成功后就已经完成了TCP三次握手。而对于服务器端,有些操作必须要在listen前调用完成,因为连接socket由accept调用返回,accept在listen监听队列中选择的接收连接至少都已经完成了TCP的三次握手的前两个握手动作进入SYN_RCVD状态

系统对于发送和接收缓冲区有一个最小值,设定值小于该值会被默认为最小值

客户端

tcpdump -nt -i eth0 port 255

  • tcpdump 命令行网络抓包工具,用于捕获和分析网络数据包。
  • -nt
    • -n 表示禁用主机名解析,只显示 IP 地址。
    • -t 表示不显示时间戳。
  • -i eth0
    • -i 后接网络接口的名称,这里是 eth0,表示捕获该网络接口上的数据包。
  • port 255
    • 表示捕获目标端口号为 255 的数据包。
    • port 后接端口号,可以是单个端口、一系列端口范围,或者使用服务名(例如,http)。

综合起来,该命令的含义是在 eth0 网络接口上捕获目标端口号为 255 的数据包,同时禁用主机名解析,不显示时间戳,将捕获到的数据包的源和目标 IP 地址以及端口号显示出来。