5. Linux网络编程基础API
5.4 监听socket
代码清单:
c
#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 <iostream>
#include <errno.h>
static bool stop = false;
/*SIGTERM信号的处理函数,触发时结束主程序中的循环*/
static void handle_term(int sig) {
stop = true;
}
int main(int argc, char* argv[]) {
signal(SIGTERM, handle_term);
if(argc <= 3) {
std::cout << "usage: " << argv[0] << " ip_address port_number backlog" << std::endl;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int backlog = atoi(argv[3]);
/*socket函数:
domain:指定协议族,这里是IPV4
type:指定传输层协议,这里是TCP服务
*/
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
/*创建一个IPV4地址*/
struct sockaddr_in address;
/*bzero(void* p, size_t n),将指定地址后的n个字节清零*/
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
/*点分十进制字符串转网络字节序列整数,支持IPV4和IPV6*/
inet_pton(AF_INET, ip, &address.sin_addr);
/*主机字节序列转化为网络字节序列*/
address.sin_port = htons(port);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
std::cout << errno << std::endl;
assert(ret != -1);
ret = listen(sock, backlog);
assert(ret != -1);
/*循环等待连接,直到有SIGTERM信号将它中断*/
while(!stop) {
sleep(1);
}
/*关闭socket*/
close(sock);
return 0;
}
我在两台局域网上的服务器做了实验。开启监听Socket的ip地址为192.168.70.73,端口为12345
充当客户端的服务器ip地址为192.168.70.72
当我在客户端服务器发起了7个连接请求后,可以发现此时只有6个请求建立了连接。
这个是客户端的,所以是SYN_SENT
5.5 接受连接
下面的系统调用从listen监听队列接受一个连接:
arduino
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen)
addr用来接收被服务器接受连接的远端socket地址。
sockfd是执行过listen系统调用的监听socket;
accept成功时返回一个新的连接socket,该socket唯一地标识了被接收的这个连接。
测试accept连接代码:
c
#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 <iostream>
#include <errno.h>
static bool stop = false;
/*SIGTERM信号的处理函数,触发时结束主程序中的循环*/
static void handle_term(int sig) {
stop = true;
}
int main(int argc, char* argv[]) {
signal(SIGTERM, handle_term);
if(argc <= 3) {
std::cout << "usage: " << argv[0] << " ip_address port_number backlog" << std::endl;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int backlog = atoi(argv[3]);
/*socket函数:
domain:指定协议族,这里是IPV4
type:指定传输层协议,这里是TCP服务
*/
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
/*创建一个IPV4地址*/
struct sockaddr_in address;
/*bzero(void* p, size_t n),将指定地址后的n个字节清零*/
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
/*点分十进制字符串转网络字节序列整数,支持IPV4和IPV6*/
inet_pton(AF_INET, ip, &address.sin_addr);
/*主机字节序列转化为网络字节序列*/
address.sin_port = htons(port);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
std::cout << "errno number:" << errno << std::endl;
assert(ret != -1);
ret = listen(sock, backlog);
assert(ret != -1);
/*暂停10秒等待科幻段连接和相关操作(掉线或退出)完成*/
sleep(20);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
/*client用来获取被接受连接的远端socket地址*/
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd < 0) {
std::cout << "errno is: " << errno << std::endl;
}
else {
/*接受连接成功则打印出客户端的IP地址和端口号*/
char remote[INET_ADDRSTRLEN];
std::cout << "connected with ip: " << inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN) <<
" and port: " << ntohs(client.sin_port) << std::endl;
}
return 0;
}
实验发现,即使客户端发起请求后断开连接,也可以正常打印信息。
5.6 发起连接
客户端需要通过以下系统调用来主动与服务器建立连接:
arduino
#include <sys/types.h>
#Include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen)
sockfd是本地用socket系统调用返回的一个socket;'
serv_addr指向要连接的服务器的socket地址。
connect成功时返回0。一旦成功建立连接,sockfd就唯一的标识了这个连接。客户端可以通过读写sockfd来与服务器通信。
5.7 关闭连接
关闭一个连接实际上就是关闭这个连接对应的socket:
arduino
#include <unistd.h>
int close(int fd);
不过close并不总是立即关闭这个连接,而是将fd的引用计数减一。当引用计数为0时,才真正地关闭连接。在多进程程序中,一次fork系统调默认将使父进程打开的socket的引用计数加1。
如果要立即关闭连接,则应该使用shutdown系统调用。
arduino
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
5.8 数据读写
5.8.1 TCP数据读写
对于TCP流数据的读写的系统调用是:
arduino
#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即可。
recv返回实际读取到的数据,它可能小于我们期望的长度,这时候需要多次读取。
send成功时返回实际写入的数据的长度。
flags参数的可选值,以下仅列出部分
选项名 | 含义 | SEND | RECV |
---|---|---|---|
MSG_DONTWAIT | 对socket的此次操作是非阻塞的 | Y | Y |
MSG_OOB | 发送或者接收紧急数据 | Y | Y |
需要注意的是flags参数只对当前的调用有效。
发送代码:
c
#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 );
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;
}
接收代码:
arduino
#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;
}
实验结果:
发送数据
接收数据
Tcpdump抓取的数据包:
补充TCP的连接与分手
5.8.2 UDP数据读写
5.8.3 通用数据读写
5.11 socket选项
下面两个系统调用是专门用来读取和设置socket文件描述符属性的方法:
restrict关键字:告诉编译器该变量已经被指针修饰,不能通过其它直接或间接方式来修改它。
c
#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参数指定被操作的目标socket;
level参数指定要操作哪个协议的选项;
option_name参数则指定选项的名字;
option_value和option_len指定被操作选项的值和长度。
因为有些设置是在TCP的同步报文时设置的。对于服务端而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效;对于客户端而言,有些socket选项应该在调用connect函数之前设置。
5.11.1 SO_RCVBUF和SO_SNDBUF选项
以上两个选项分别表示接收缓冲区和发送缓冲区的大小。当我们用setsocket来设置TCP的接收缓冲区和发送缓冲区的大小的时,系统都会将其值加倍,并且不得小于某个最小值。 TCP接收缓冲区的最小值是256字节,发送缓冲区的最小值是2048字节(不同系统可能不一样)
修改TCP接收缓冲区的服务器程序
c
#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;
}
/*接收点分十进制字符串ip地址和端口号*/
const char* ip = argv[1];
int port = atoi( argv[2] );
/*设置ipv4地址*/
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 );
/*SOL_SOCKET表示通用socket选项,与协议无关*/
printf( "the receive buffer size before settting is %d\n", 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 ];
/*void *memset(void *buffer, int c, int count), c是被设置的值,count是缓冲区大小*/
memset( buffer, '\0', BUFFER_SIZE );
while( recv( connfd, buffer, BUFFER_SIZE-1, 0 ) > 0 ){}
close( connfd );
}
close( sock );
return 0;
}
修改TCP发送缓冲区的客户端程序
c
#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 );
printf( "the tcp send buffer size before setting is %d\n", sendbuf );
setsockopt( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof( sendbuf ) );
getsockopt( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, ( socklen_t* )&len );
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;
}
服务器接收端:
客户端发送端:
Tcpdump抓包数据:
5.11.2 SO_REVLOWAT和SO_SNDLOWAT选项
两选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用用来判断socket是否可读可写。当TCP接收缓冲区中可读数据的综述大于其低水位标记时,I/O复用系统将通知应用程序可以从其对应的socket上读取数据;当TCP发送缓冲区的空闲空间大于低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socket上写入数据。