Linux的socket API基本操作

1.socket地址API

主机字节序和网络字节序

字节序分为大端字节序和小端字节序

  • 大端字节序 = 一个整数的高位字节存储在内存的低地址,低位字节存储在内存的高地址

    通信的时候总采用大端字节序,也称为网络字节序

  • 小端字节序 = 一个整数的低位字节存储在内存的高地址,低位字节存储在内存的低地址

    PC一般采用小端字节序,所以也称为主机字节序

发送方总是把要发送的数据转化成大端字节序数据然后发送,而接受端知道对方传送过来的数据总是采用大端字节序,并根据自身采用的字节序决定是否对接收到的数据进行转换。

一个判断机器字节序的程序:

# include

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" )
    }
}

linux提供了四个函数来完成主机字节序与网络字节序间的转换:

#include 

//htonl = host to network long
unsigned long int htonl( unsigned long int hostlong );
unsigned short int htons( unsigned short int hostshort );
unsigned long int ntohl( unsigned long int netlong );
unsigned short int ntohs( unsigned short int netshort );

通用socket地址

用来表示socket地址的是结构体sockaddr,定义如下:

#include 
struct sockaddr
{
    //socket address family type,地址族类型
    sa_family_t sa_family;
    //14位包含了地址和端口
    char sa_date[14];
}

地址族 = AF = address family

协议组 = PF = protocol family

专用socket地址

linux还为各个协议族提供了专门的socket地址结构体

IP地址转换函数

点分十进制转为网络字节序整数表示的IPV4地址:

#include 
in_addr_t inet_addr( const char* strptr);
int inet_aton( const char* cp, struct in_addr* inp);
char* inet_ntoa( struct in_addr in );

2.创建socket

Linux中所有东西都是文件,socket是一种可读、可写、可控制、可关闭的文件描述符。

socket的创建:

#include 
#include 
int socket( int domain, int type, int protocol );
  • domain:告诉系统使用哪个底层协议组

    TCP/IPPF_INET(Protocol Family of Internet)(IPV4)

    ​ PF_INET6(IPV6)

    Unix本地域协议族:PF_UNIX

  • type:指定服务类型

    TCP流服务SOCK_STREAM

    UDP数据报服务:SOCK_UGRAM

  • protocol:在前两个参数构成的协议集合下,再选择一个具体的协议

    大部分情况下都设为0

socket系统调用成功时返回一个socket描述符,失败则返回-1并设置errno

3.命名socket

通过 bind 来命名 socket:

#include 
#include 
int bind( int sockfd, const struct sockaddr* my_addr, socklen_tt addrlen );

bind 将my_addr所指的socket分配给未命名的sockfd文件描述符。

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

4.监听socket

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

#include 
int listen( int sockfd, int backlog );
  • sockfd:指定被监听的socket

  • backlog:提示内核监听队列的最大长度

    监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端将收到ECONNREFUSED错误信息

/* SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和
处理. 通常用来要求程序自己正常退出. shell命令kill缺省产生这
个信号. */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

//静态全局变量:定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见
static bool stop = 0;
/* SIGTERM信号的处理函数,触发时结束主程序中的循环 */
static void handle_term( int sig )
{
    stop = true;
}

int main( int argc, char* argv[] )
{
    signal( SIGTERM, handle_term);
    
    if( argc <= 3)
    {
        //argv[0]是程序名
        printf( "usage: %s ip_address port_number backlog\n", basename( argv[0] ) ); //basename:去除字符串中的目录部分
        return 1;
    }
    //argv[1]是ip地址
    const char* ip = argv[1];
    //argv[2]是端口地址
    //atoi = ascii to integer
    int port = atoi( argv[2] );
    //argv[3]是backlog
    int backlog = atoi( argv[3] );
    
    //创建一个socket,采用TCP/IP协议组,TCP流服务
    int sock = socket( PF_INET, SOCK_STREAM, 0);
    //如果socket没有创建成功则终止程序
    assert( sock >= 0 );
    
    /* 创建一个 IPV4 socket 地址 */
    struct socketaddr_in address;
    //置字节字符串前n个字节为零且包括‘\0’
    //似乎是把address内容全部置0的意思
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    //讲ip地址由点分十进制串转换成网络字节序二进制值
    //反过来就是ntop
    inet_pton( AF_INET, ip, &address.sin_addr );
    //将port主机序转网络序,host to net short
    address.sin_port = htons( port );
    
    int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ));
    assert( ret != -1 );
    
    ret = listen( sock, backlog );
    assert( ret != -1 );
    
    /* 循环等待连接,知道有SIGTERM信号将它中断 */
    while ( !stop )
    {
        sleep( 1 );
    }
    
    /* 关闭socket */
    close( sock );
    return 0;
}

argc 是 argument count的缩写,表示传入main函数的参数个数

argv 是 argument vector的缩写,表示传入main函数的参数序列或指针,并且第一个参数argv[0]一定是程序的名称,并且包含了程序所在的完整路径,所以确切的说需要我们输入的main函数的参数个数应该是argc-1个

struct的sizeof问题:

1:数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。

2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)

3:收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐.

5.接受连接

从listen监听队列中接受一个连接:

#include 
#include 
int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen );
  • sockfd:执行过listen系统调用的监听socket
  • addr:被接受连接的远端socket地址
  • addrlen:指出socket的地址长度

accept成功时,返回一个新的socket,该socket唯一地标识了被接受的这个连接,服务器可以通过读写该socket来与被接受连接对应的客户端通信;accept失败时,返回-1并设置errno

accept只是从监听队列中取出连接,而不论连接处于何种状态,也不关心任何网络状况的变化

6.发起连接

客户端通过如下系统调用来主动与服务器建立连接:

#include 
#include 
int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen );
  • sockfd:socket系统调用返回的一个socket
  • serv_addr:服务器监听的socket地址

connect成功时返回0,当连接成功建立,sockfd就唯一地标识了该连接,客户端可以通过读写sockfd来与服务器通信。

connect失败则返回-1,并设置errno,常见的两种是:

  • ECONREFUSED:目标端口不存在,连接被拒绝
  • ETIMEDOUT:连接超时

7.关闭连接

#include 
int close( int fd );

fd:待关闭的socket

close系统调用并非总是关闭一个连接,而是将fd的引用计数 -1,当fd的引用计数为0时,才真的关闭连接。

多进程程序中,一次fork系统调用将使得父进程中打开的socket引用计数+1,因此我们必须在父进程和子进程中都对该socket执行close才能关闭连接。

如果要立即终止连接而不是将socket引用计数-1,可以使用如下的shutdown系统调用:

#include 
int shutdown( int sockfd, int howto );

你可能感兴趣的:(Linux的socket API基本操作)