1. 程式人生 > >網路程式設計教程(四)Linux網路程式設計基礎API

網路程式設計教程(四)Linux網路程式設計基礎API

        首先介紹Linux下整個的網路程式設計流程:

一、socket地址API

1.主機位元組序和網路位元組序

        位元組序分為大端位元組序(big endian)和小端位元組序(little endian)。大端位元組序是指一個整數的搞我位元組儲存在記憶體的低地址處,低位位元組儲存在記憶體的高地址處。小端位元組序則是整數的高位位元組儲存在記憶體的高地址處,而低位位元組則儲存在記憶體的低地址處。

        一般,大端位元組序也稱為網路位元組序,小端位元組序也稱為主機位元組序。

#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlog);//將長整型的主機位元組序資料轉為網路位元組序資料
unsigned short int htons(unsigned short int hostshort);  //
unsigned long  int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

2.通用socket地址

#include <bits/socket.h>
struct sockaddr
{
        sa_family_t sa_family;    //地址族
        char sa_data[14];         
};

        

3.專用socket地址

       上面這個通用socket地址結構體顯然很不好用,比如設定與獲取IP地址和埠號就需要執行繁瑣的位操作。TCP/IP協議族有sockaddr_in和sockaddr_in6兩個專用socket地址結構體,他們分別用於IPv4和IPv6,這裡只介紹常用的IPv4socket地址:

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地址,要用網路位元組序表示
};

        所有專用socket地址型別的變數在實際使用時都需要轉化為通用socket地址型別sockaddr(強制型別轉換),因為所有socket編成介面使用的地址引數型別都是sockaddr。

4.IP地址轉換函式

#include <arpa/inet.h>

//將用點分十進位制字串表示的IPv4地址轉化為網路位元組序整數表示的IPv4地址,失敗時返回INADDR_NONE.
in_addr_t inet_addr(const char* strptr);

//將點分十進位制字串表示的IPv4地址轉化為網路位元組序表示的IPv4地址,存放於inp中
int inet_aton(const char *cp, struct in_addr *inp);

//將用網路位元組序整數表示的IPv4地址轉化為用點分十進位制字串表示的IPv4地址
char* inet_ntoa(struct in_addr in);

//將字串表示的IP地址src轉為網路位元組序整數表示的IP地址,並存放在dst中,其中af指定地址族
int inet_pton(int af, const char* src, void *dst);

//與上面相反
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);


二、socket基礎API

1.建立socket

        socket是一個可讀、可寫、可控制和可關閉的檔案描述符。下面的socket系統呼叫建立一個socket:

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

引數:
    domain  :指定協議族,一般用PF_INET
    type    :指定服務型別,主要有SOCK_STREAM服務(流服務)和SOCK_UGRAM(資料報)服務
    protocol:一般設定為0,表示使用預設協議。因為前兩個引數已經唯一確定了是使用TCP還是UDP協議

返回值:
    socket系統呼叫成功時返回一個socket檔案描述符,失敗則返回-1並設定errno.

2.命名socket

         建立socket時,指定了地址族,但是沒有指定具體使用哪個socket地址,將一個socket與socket地址繫結稱為給socket命名。在伺服器程式中要命名socket,因為只有命名後客戶端才能知道該如何連線它。客戶端通常不需要命名socket,而是採用匿名方式,即使用作業系統自動分配的socket地址。

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

功能:
    將my_addr所指的socket地址分配給未命名的sockfd
引數:
    sockfd :呼叫socket()建立的socket檔案描述符
    my_addr:socket地址
    addrlen:my_addr的長度
返回值:
    bind成功時返回0,失敗則返回-1,並設定errno.其中兩種常見的errno是EACCES和EADDRINUSE,EACCES是指被繫結的地址是受保護的
地址,而EADDRINUSE是指被繫結的地址正在使用中。

3.監聽socket

        socket被命名以後,還不能馬上接受客戶連線,需要使用如下系統呼叫建立一個監聽佇列以存放待處理的客戶連線:

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

功能:
    建立一個監聽佇列用於存放待處理的連線
引數:
    sockfd :指定被監聽的socket
    backlog:指示核心監聽佇列的最大長度
返回值:
    listen成功時返回0,失敗則返回-1並設定errno。

4.接收連線

        每當連線到來時就會被放入listen()系統呼叫建立的監聽佇列中,這時需要呼叫accept()從監聽佇列中接受一個連線:

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

功能:
    從listen監聽佇列中接受一個連線
引數:
    sockfd :執行過listen系統呼叫的監聽socket。
    addr   :用於獲取被接受連線的源端socket地址
    addrlen:指定addr的長度
返回值:
    成功時返回一個新的連線socket,該socket唯一地標識了被接受的這個連線,伺服器可以通過讀寫該socket來與被接受連線的客戶端通訊。失敗時返回-1並設定errno.

5.發起連線

        伺服器通過listen呼叫來被動接受連線,那麼客戶端需要通過connect()系統呼叫來與伺服器建立連線:

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servAddr, socklen_t addrlen);

功能:
    與伺服器建立連線
引數:
    sockfd  :由socket()系統呼叫返回
    servAddr:伺服器監聽的socket地址
    addrlen :指定servAddr的長度
返回值:
    connect成功時返回0.一旦成功建立連線,sockfd就唯一標識了這個連線,客戶端就可以通過讀寫該sockfd來與伺服器通訊。connect失敗時返回-1並設定errno,兩種常見的errno是ECONNREFUSED和ETIMEOUT,ECONNREFUSED表示目標埠不存在,ETIMEOUT表示連線超時。

6.關閉連線

       關閉一個連線實際上是關閉該連線對應的socket。

#include <unistd.h>
int close(int fd);

功能:
    關閉檔案描述符。close系統呼叫並非總是關閉一個連線,而是將fd引用計數減1,只有當fd引用計數為0時,才真正關閉連線。
引數:
    fd:指定要關閉的檔案描述符
返回值:
    成功時返回0,失敗時返回-1並設定errno.

三、資料讀寫

1.TCP資料讀寫

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:
    從sockfd中讀取len位元組的資料到buf中
引數:
    sockfd:建立連線後的檔案描述符
    buf   :讀緩衝區的位置
    len   :讀緩衝區的大小
    flags :一般設定為0
返回值:
    recv成功時返回實際讀取到的資料的長度,它可能返回0,這意味著通訊對方已經關閉連線,出錯時返回-1並設定errno.

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:
    將buf中len位元組資料寫入sockfd中
引數:
    sockfd:建立連線後的檔案描述符
    buf   :寫緩衝區的位置
    len   :寫緩衝區的大小
    flags :一般設定為0
返回值:
    send成功時返回實際寫入資料的長度,失敗時則返回-1並設定errno.

2.UDP資料讀寫

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, 
                 struct sockaddr *srcaddr, socklen_t *addrlen);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               struct sockaddr *destaddr, socklen_t addrlen);

3.通用資料讀寫函式

#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);

4.socket選項

socket選項 作用
SO_REUSEADDR 強制使用被處於TIME_WAIT狀態的連線佔用的socket地址
SO_RCVBUF 設定接收緩衝區大小
SO_SNDBUF 設定傳送緩衝區大小
SO_RCVLOWAT 接收緩衝區的低水位標記
SO_SNDLOWAT 傳送緩衝區的低水位標記
SO_LINGER 控制close()系統呼叫在關閉TCP連線時的行為

四、例項程式碼分析

客戶端程式碼:

#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 );   //建立socket,使用TCP協議
    assert( sockfd >= 0 );

    //發起連線
    int ret = connect( sockfd, (struct sockaddr*)&server_address, sizeof(server_address));
    if(ECONNREFUSED == ret)
    {
        printf("目標埠不存在,連線被拒絕.\n");
    }
    else if(ETIMEOUT == ret)
    {
        printf("連線超時.\n");
    }
    else if(ret < 0)
    {
        printf("connetion failed.\n");
    }
    else
    {
        printf( "send oob data out\n" );
        const char* normal_data = "123";
        send( sockfd, normal_data, strlen( normal_data ), 0 ); //傳送資料
    }

    close( sockfd );   //關閉連線
    return 0;
}

服務端程式碼:

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

const int 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 );   //建立socket,使用TCP協議
    assert( sock >= 0 );

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

    //監聽socket
    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 );

        close( connfd );   //關閉連線socket
    }

    close( sock );   //關閉監聽socket
    return 0;
}