socket編程之----TCP服務器


1、socket編程
socket這個詞能夠表不少概念: TCP/IP協議中,「IP地址+TCPUDP惟標識絡通信中的個進程,「IP地址+就稱爲socket
TCP協議中,建鏈接的兩個進程各有個socket來標識,那麼這兩個socket組成 的socket pair就惟標識個鏈接。 socket本有插座的意思,所以來描述絡鏈接的 對關係。
TCP/IP協議最先在BSD UNIX上實現,TCP/IP協議設計的應層編程接稱爲socket APIios

2、相關函數:編程

socket 函數數組

套接字是通訊端點的抽象,實現端對端之間的通訊。與應用程序要使用文件描述符訪問文件同樣,訪問套接字須要套接字描述符。任何套接字編程都必須調用socket 函數得到套接字描述符,這樣才能對套接字進行操做。如下是該函數的描述:服務器

1. /* 套接字 */   網絡

2.    併發

3. /*  socket

4.  * 函數功能:建立套接字描述符;  tcp

5.  * 返回值:若成功則返回套接字非負描述符,若出錯返回-1;  ide

6.  * 函數原型:  函數

7.  */   

8. #include <sys/socket.h>   

9.    

10. int socket(int family, int type, int protocol);   

11. /*  

12.  * 說明:  

13.  * socket相似與open對普通文件操做同樣,都是返回描述符,後續的操做都是基於該描述符;  

14.  * family 表示套接字的通訊域,不一樣的取值決定了socket的地址類型,其通常取值以下:  

15.  * (1)AF_INET         IPv4因特網域  

16.  * (2)AF_INET6        IPv6因特網域  

17.  * (3)AF_UNIX         Unix域  

18.  * (4)AF_ROUTE        路由套接字  

19.  * (5)AF_KEY          密鑰套接字  

20.  * (6)AF_UNSPEC       未指定  

21.  *  

22.  * type肯定socket的類型,經常使用類型以下:  

23.  * (1)SOCK_STREAM     有序、可靠、雙向的面向鏈接字節流套接字  

24.  * (2)SOCK_DGRAM      長度固定的、無鏈接的不可靠數據報套接字  

25.  * (3)SOCK_RAW        原始套接字  

26.  * (4)SOCK_SEQPACKET  長度固定、有序、可靠的面向鏈接的有序分組套接字  

27.  *  

28.  * protocol指定協議,經常使用取值以下:  

29.  * (1)0               選擇type類型對應的默認協議  

30.  * (2)IPPROTO_TCP     TCP傳輸協議  

31.  * (3)IPPROTO_UDP     UDP傳輸協議  

32.  * (4)IPPROTO_SCTP    SCTP傳輸協議  

33.  * (5)IPPROTO_TIPC    TIPC傳輸協議  

34.  *  

35.  */   

connect 函數

在處理面向鏈接的網絡服務時,例如 TCP ,交換數據以前必須在請求的進程套接字和提供服務的進程套接字之間創建鏈接。TCP 客戶端能夠調用函數connect 來創建與 TCP 服務器端的一個鏈接。該函數的描述以下:

1. /*  

2.  * 函數功能:創建鏈接,即客戶端使用該函數來創建與服務器的鏈接;  

3.  * 返回值:若成功則返回0,出錯則返回-1;  

4.  * 函數原型:  

5.  */   

6. #include <sys/socket.h>   

7.    

8. int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);   

9. /*  

10.  * 說明:  

11.  * sockfd是系統調用的套接字描述符,即由socket函數返回的套接字描述符;  

12.  * servaddr是目的套接字的地址,該套接字地址結構必須包含目的IP地址和目的端口號,即想與之通訊的服務器地址;  

13.  * addrlen是目的套接字地址的大小;  

14.  *  

15.  * 若是sockfd沒有綁定到一個地址,connect會給調用者綁定一個默認地址,即內核會肯定源IP地址,並選擇一個臨時端口號做爲源端口號;  

16.  */   

TCP 客戶端在調用函數 connect 前沒必要非得調用 bind 函數,由於內核會肯定源 IP 地址,並選擇一個臨時端口做爲源端口號。若 TCP 套接字調用connect 函數將創建 TCP 鏈接(執行三次握手),並且僅在鏈接創建成功或出錯時才返回,其中出錯返回可能有如下幾種狀況:

若 TCP 客戶端沒有收到 SYN 報文段的響應,則返回 ETIMEOUT 錯誤;

若客戶端的 SYN 報文段的響應是 RST (表示復位),則代表該服務器主機在咱們指定的端口上沒有進程在等待與之鏈接。只是一種硬錯誤,客戶端一接收到 RST 就當即返回ECONNERFUSED 錯誤;

RST 是 TCP 在發生錯誤時發送的一種 TCP 報文段。產生 RST 的三個條件時:

目的地爲某端口的 SYN 到達,然而該端口上沒有正在監聽的服務器;

TCP 想取消一個已有鏈接;

TCP 接收到一個不存在的鏈接上的報文段;

若客戶端發出的 SYN 在中某個路由器上引起一個目的地不可達的 ICMP 錯誤,這是一個軟錯誤。客戶端主機內核保存該消息,並在必定的時間間隔繼續發送 SYN (即重發)。在某規定的時間後仍未收到響應,則把保存的消息(即 ICMP 錯誤)做爲EHOSTUNREACH 或ENETUNREACH 錯誤返回給進行。

bind 函數

調用函數 socket 建立套接字描述符時,該套接字描述符是存儲在它的協議族空間中,沒有具體的地址,要使它與一個地址相關聯,能夠調用函數bind 使其與地址綁定。客戶端的套接字關聯的地址通常可由系統默認分配,所以不須要指定具體的地址。若要爲服務器端套接字綁定地址,能夠經過調用函數 bind 將套接字綁定到一個地址。下面是該函數的描述:

1. /* 套接字的基本操做 */   

2.    

3. /*  

4.  * 函數功能:將協議地址綁定到一個套接字;其中協議地址包含IP地址和端口號;  

5.  * 返回值:若成功則返回0,若出錯則返回-1;  

6.  * 函數原型:  

7.  */   

8. #include <sys/socket.h>   

9. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);   

10. /*  

11.  * 說明:  

12.  * sockfd 爲套接字描述符;  

13.  * addr是一個指向特定協議地址結構的指針;  

14.  * addrlen是地址結構的長度;  

15.  */   

對於 TCP 協議,調用 bind 函數能夠指定一個端口號,或指定一個 IP 地址,也能夠二者都指定,還能夠都不指定。若 TCP 客戶端或服務器端不調用bind 函數綁定一個端口號,當調用connect 或 listen 函數時,內核會爲相應的套接字選擇一個臨時端口號。通常 TCP 客戶端使用內核爲其選擇一個臨時的端口號,而服務器端經過調用bind 函數將端口號與相應的套接字綁定。進程能夠把一個特定的 IP 地址捆綁到它的套接字上,可是這個 IP 地址必須屬於其所在主機的網絡接口之一。對於 TCP 客戶端,這就爲在套接字上發送的 IP 數據報指派了源 IP 地址。對於 TCP 服務器端,這就限定該套接字只接收那些目的地爲這個 IP 地址的客戶端鏈接。TCP 客戶端通常不把 IP 地址捆綁到它的套接字上。當鏈接套接字時,內核將根據所用外出網絡接口來選擇源 IP 地址,而所用外出接口則取決於到達服務器端所需的路徑。若 TCP 服務器端沒有把 IP 地址捆綁到它的套接字上,內核就把客戶端發送的 SYN 的目的 IP 地址做爲服務器端的源 IP 地址。

在地址使用方面有下面一些限制:

在進程所運行的機器上,指定的地址必須有效,不能指定其餘機器的地址;

地址必須和建立套接字時的地址族所支持的格式相匹配;

端口號必須不小於1024,除非該進程具備相應的特權(超級用戶);

通常只有套接字端點可以與地址綁定,儘管有些協議容許多重綁定;

listen 函數

在編寫服務器程序時須要使用監聽函數 listen 。服務器進程不知道要與誰鏈接,所以,它不會主動地要求與某個進程鏈接,只是一直監聽是否有其餘客戶進程與之鏈接,而後響應該鏈接請求,並對它作出處理, 一個服務進程能夠同時處理多個客戶進程的鏈接。listen 函數描述以下:

1. /*  

2.  * 函數功能:接收鏈接請求;  

3.  * 函數原型:  

4.  */   

5. #include <sys/socket.h>   

6.    

7. int listen(int sockfd, int backlog);//若成功則返回0,若出錯則返回-1;   

8. /*  

9.  * sockfd是套接字描述符;  

10.  * backlog是該進程所要入隊請求的最大請求數量;  

11.  */   

listen 函數僅由 TCP 服務器調用,它有如下兩種做用:

當 socket 函數建立一個套接字時,若它被假設爲一個主動套接字,即它是一個將調用connect 發起鏈接的客戶端套接字。listen 函數把一個未鏈接的套接字轉換成一個被動套接字,指示內核應該接受指向該套接字的鏈接請求;

listen 函數的第二個參數規定內核應該爲相應套接字排隊的最大鏈接個數;

listen 函數通常應該在調用socket 和bind 這兩個函數以後,並在調用 accept 函數以前調用。 內核爲任何一個給定監聽套接字維護兩個隊列:

未完成鏈接隊列,每一個這樣的 SYN 報文段對應其中一項:已由某個客戶端發出併到達服務器,而服務器正在等待完成相應的 TCP 三次握手過程。這些套接字處於 SYN_REVD 狀態;

已完成鏈接隊列,每一個已完成 TCP 三次握手過程的客戶端對應其中一項。這些套接字處於 ESTABLISHED 狀態;

accept 函數

accept 函數由 TCP 服務器調用,用於從已完成鏈接隊列隊頭返回下一個已完成鏈接。若是已完成鏈接隊列爲空,那麼進程被投入睡眠。該函數的返回值是一個新的套接字描述符,返回 值是表示已鏈接的套接字描述符,而第一個參數是服務器監聽套接字描述符。一個服務器一般僅僅建立一個監聽套接字,它在該服務器的生命週期內一直存在。內核 爲每一個由服務器進程接受的客戶鏈接建立一個已鏈接套接字(表示 TCP 三次握手已完成),當服務器完成對某個給定客戶的服務時,相應的已鏈接套接字就會被關閉。該函數描述以下:

1. /* 函數功能:從已完成鏈接隊列隊頭返回下一個已完成鏈接;若已完成鏈接隊列爲空,則進程進入睡眠;  

2.  * 函數原型:  

3.  */   

4. int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);//返回值:若成功返回套接字描述符,出錯返回-1;   

5. /*  

6.  * 說明:  

7.  * 參數 cliaddr 和 addrlen 用來返回已鏈接的對端(客戶端)的協議地址;  

8.  *  

9.  * 該函數返回套接字描述符,該描述符鏈接到調用connect函數的客戶端;  

10.  * 這個新的套接字描述符和原始的套接字描述符sockfd具備相同的套接字類型和地址族,而傳給accept函數的套接字描述符sockfd沒有關聯到這個連接,  

11.  * 而是繼續保持可用狀態並接受其餘鏈接請求;  

12.  * 若不關心客戶端協議地址,可將cliaddr和addrlen參數設置爲NULL,不然,在調用accept以前,應將參數cliaddr設爲足夠大的緩衝區來存放地址,  

13.  * 而且將addrlen設爲指向表明這個緩衝區大小的整數指針;  

14.  * accept函數返回時,會在緩衝區填充客戶端的地址並更新addrlen所指向的整數爲該地址的實際大小;  

15.  *  

16.  * 若沒有鏈接請求等待處理,accept會阻塞直到一個請求到來;  

fork 和 exec 函數

1. /* 函數功能:建立子進程;  

2.  * 返回值:  

3.  * (1)在子進程中,返回0;  

4.  * (2)在父進程中,返回新建立子進程的進程ID;  

5.  * (3)若出錯,則範回-1;  

6.  * 函數原型:  

7.  */   

8. #include <unistd.h>   

9. pid_t fork(void);   

10. /* 說明:  

11.  * 該函數調用一次若成功則返回兩個值:  

12.  * 在調用進程(即父進程)中,返回新建立進程(即子進程)的進程ID;  

13.  * 在子進程返回值是0;  

14.  * 所以,能夠根據返回值判斷進程是子進程仍是父進程;  

15.  */   

16.    

17. /* exec 序列函數 */   

18.    

19. /*  

20.  * 函數功能:把當前進程替換爲一個新的進程,新進程與原進程ID相同;  

21.  * 返回值:若出錯則返回-1,若成功則不返回;  

22.  * 函數原型:  

23.  */   

24. #include <unistd.h>   

25. int execl(const char *pathname, const char *arg, ...);   

26. int execv(const char *pathnam, char *const argv[]);   

27. int execle(const char *pathname, const char *arg, ... , char *const envp[]);   

28. int execve(const char *pathnam, char *const argv[], char *const envp[]);   

29. int execlp(const char *filename, const char *arg, ...);   

30. int execvp(const char *filename, char *const argv[]);   

31. /*  6 個函數的區別以下:  

32.  * (1)待執行的程序文件是 文件名 仍是由 路徑名 指定;  

33.  * (2)新程序的參數是 一一列出 仍是由一個 指針數組 來引用;  

34.  * (3)把調用進程的環境傳遞給新程序 仍是 給新程序指定新的環境;  

35.  */   

exec 6個函數在函數名和使用語法的規則上都有細微的區別,下面就從可執行文件查找方式、參數傳遞方式及環境變量這幾個方面進行比較。

查找方式:前4個函數的查找方式都是完整的文件目錄路徑 pathname ,而最後兩個函數(也就是以p結尾的兩個函數)能夠只給出文件名 filename,系統就會自動按照環境變量 「$PATH」 所指定的路徑進行查找。

參數傳遞方式:exec 序列函數的參數傳遞有兩種方式:一種是逐個列舉的方式,而另外一種則是將全部參數總體構造指針數組傳遞。在這裏是以函數名的第5位字母來區分的,字母爲 「l」(list)的表示逐個列舉參數的方式,其語法爲 const char *arg;字母爲 「v」(vertor)的表示將全部參數總體構造指針數組傳遞,其語法爲 char *const argv[]。讀者能夠觀察 execl()、execle()、execlp() 的語法與 execv()、execve()、execvp() 的區別。這裏的參數實際上就是用戶在使用這個可執行文件時所需的所有命令選項字符串(包括該可執行程序命令自己)。要注意的是,這些參數必須以NULL結 束。

環境變量:exec 序列函數能夠默認系統的環境變量,也能夠傳入指定的環境變量。這裏以 「e」(environment)結尾的兩個函數 execle() 和 execve() 就能夠在 envp[] 中指定當前進程所使用的環境變量。

1.  表 1 exec 序列函數的總結       

2. 前4位 統一爲:exec       

3. 第5位 l:參數傳遞爲逐個列舉方式    execl、execle、execlp   

4.      v:參數傳遞爲構造指針數組方式     execv、execve、execvp   

5. 第6位 e:可傳遞新進程環境變量     execle、execve   

6.      p:可執行文件查找方式爲文件名     execlp、execvp   


併發服務器

當要求一個服務器同時爲多個客戶服務時,須要併發服務器。TCP 併發服務器,它們爲每一個待處理的客戶端鏈接調用 fork 函數派生一個子進程。當一個鏈接創建時,accept 返回,服務器接着調用 fork 函數,而後由子進程服務客戶端,父進程則等待另外一個鏈接,此時,父進程必須關閉已鏈接套接字。

close 和 shutdown 函數

當要關閉套接字時,可以使用 close 和 shutdown 函數,其描述以下:

1. /* 函數功能:關閉套接字,如果在 TCP 協議中,並終止 TCP 鏈接;  

2.  * 返回值:若成功則返回0,若出錯則返回-1;  

3.  * 函數原型:  

4.  */   

5. #include <unistd.h>   

6. int close(int sockfd);   

7.    

8. /*  

9.  * 函數功能:關閉套接字上的輸入或輸出;  

10.  * 返回值:若成功則返回0,若出錯返回-1;  

11.  * 函數原型:  

12.  */   

13. #include <sys/socket.h>   

14. int shutdown(int sockfd, int how);   

15. /*  

16.  * 說明:  

17.  * sockfd表示待操做的套接字描述符;  

18.  * how表示具體操做,取值以下:  

19.  * (1)SHUT_RD     關閉讀端,即不能接收數據  

20.  * (2)SHUT_WR     關閉寫端,即不能發送數據  

21.  * (3)SHUT_RDWR   關閉讀、寫端,即不能發送和接收數據  

22.  *  

23.  */   

getsockname 和 getpeername 函數

爲了獲取已綁定到套接字的地址,咱們能夠調用函數 getsockname 來實現:

1. /*  

2.  * 函數功能:獲取已綁定到一個套接字的地址;  

3.  * 返回值:若成功則返回0,若出錯則返回-1;  

4.  * 函數原型:  

5.  */   

6. #include <sys/socket.h>   

7.    

8. int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);   

9. /*  

10.  * 說明:  

11.  * 調用該函數以前,設置alenp爲一個指向整數的指針,該整數指定緩衝區sockaddr的大小;  

12.  * 返回時,該整數會被設置成返回地址的大小,若是該地址和提供的緩衝區長度不匹配,則將其截斷而不報錯;  

13.  */   

14. /*  

15.  * 函數功能:獲取套接字對方鏈接的地址;  

16.  * 返回值:若成功則返回0,若出錯則返回-1;  

17.  * 函數原型:  

18.  */   

19. #include <sys/socket.h>   

20.    

21. int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp);   

22. /*  

23.  * 說明:  

24.  * 該函數除了返回對方的地址以外,其餘功能和getsockname同樣;  

25.  */   

3、相關代碼及運行結果:

 //tcp_server.cpp

#include<iostream>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<errno.h>
#include<string.h>
#include<string>
#include<pthread.h>
using namespace std;

const int g_backlog=5;

void usage(string _proc)
{
    cout<<"Usage:"<<_proc<<"[ip] [port]"<<endl;
}

static int startup(const string &ip,const int &port)
{
    //1.
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        cerr<<strerror(errno)<<endl;
        exit(1);
    }
    //2
    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(port);
    local.sin_addr.s_addr=inet_addr(ip.c_str());

    //3
    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
        cerr<<strerror(errno)<<endl;
        exit(2);
    }

    //4
    if(listen(sock,g_backlog)<0)
    {
        cerr<<strerror(errno)<<endl;
        exit(3);
    }
    //5
    return sock;
}

void *thread_run(void *arg)
{
    int sock=(int)arg;
    char buf[1024];
    while(1)
    {
        memset(buf,'\0',sizeof(buf));
        ssize_t _size=read(sock,buf,sizeof(buf)-1);
        if(_size>0)//read success
        {
            buf[_size]='\0';
        }
        else if(_size==0)
        {
            cout<<"client close..."<<endl;
            break;
        }
        else
        {
            cout<<strerror(errno)<<endl;
        }
        cout<<"client#"<<buf<<endl;
    }
    close(sock);
    return NULL;
}

int main(int argc,char *argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    string ip=argv[1];
    int port=atoi(argv[2]);
    int listen_sock=startup(ip,port);

    struct sockaddr_in client;
    socklen_t len =sizeof(client);
    while(1)
    {
        int new_sock=accept(listen_sock,(struct sockaddr*)&client,&len);
        if(new_sock<0)
        {
            cerr<<strerror(errno)<<endl;
            continue;
        }
        cout<<"get a connect..."<<"sock :"<<new_sock\
            <<"ip :"<<inet_ntoa(client.sin_addr)<<"port :"\
            <<ntohs(client.sin_port)<<endl;

#ifdef _v1_
        //version 1
        char buf[1024];
        while(1)
        {
            ssize_t _size=read(new_sock,buf,sizeof(buf)-1);
            if(_size>0)//read success
            {
                buf[_size]='\0';
            }
            else if(_size==0)//client close
            {}
            else
            {
                cout<<strerror(errno)<<endl;
            }
            cout<<"client#"<<buf<<endl;
        }
#elif _v2_
        cout<<"v2"<<endl;
        pid_t id=fork();
        if(id==0)//child
        {
            std::string _client=inet_ntoa(client.sin_addr);
            close(listen_sock);
            char buf[1024];
            while(1)
            {
                memset(buf,'\0',sizeof(buf));
                ssize_t _size=read(new_sock,buf,sizeof(buf)-1);
                if(_size>0)//read success
                {
                    buf[_size]='\0';
                }
                else if(_size==0)
                {
                    cout<<_client<<"close..."<<endl;
                    break;
                }
                else
                {
                    cout<<strerror(errno)<<endl;
                }
                cout<<_client<<"#"<<buf<<endl;
            }
            close(new_sock);
            exit(0);
        }
        else if(id>0)//father
        {
            close(new_sock);
        }
        else
        {}
#elif _v3_
        pthread_t tid;
        pthread_create(&tid,NULL,thread_run,(void*)new_sock);
        pthread_detach(tid);
#else
        cout<<"default"<<endl;
#endif
    }
    return 0;
}


//tcp_client.cpp

#include<iostream>
#include<string>
#include<unistd.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<string.h>
using namespace std;

void usage(string _proc)
{
    cout<<_proc<<"[remote ip] [remote port]"<<endl;
}

int main(int argc,char *argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    int r_port =atoi(argv[2]);
    string r_ip =argv[1];

    int sock =socket(AF_INET,SOCK_STREAM,0);
    if(sock<-1)
    {
        cout<<strerror(errno)<<endl;
        exit(1);
    }

    struct sockaddr_in remote;
    remote.sin_family=AF_INET;
    remote.sin_port=htons(r_port);
    remote.sin_addr.s_addr=inet_addr(r_ip.c_str());

    int ret=connect(sock,(struct sockaddr*)&remote,sizeof(remote));
    if(ret<0)
    {
        cout<<strerror(errno)<<endl;
    }

    string msg;
    while(1)
    {
        cout<<"Please Enter:";
        cin>>msg;
        write(sock,msg.c_str(),msg.size());
    }
    return 0;
}

運行結果爲:

wKiom1d0hWSDqBhqAABMN01W6jQ357.png

相關文章
相關標籤/搜索