1. 程式人生 > >TCP/IP(7)-TCP Server與TCP Client(linux套接字)

TCP/IP(7)-TCP Server與TCP Client(linux套接字)

前面幾篇文章談到的關於TCP/IP應用層以下的協議,這些協議最終是在作業系統核心中實現的,套接字API是unix系統用於網路連線的介面,後來被移植到windows系統中,就有了winsock。

TCP的Client/Server模式

在TCP/IP協議中已經講解了TCP協議中三次握手和四次握手過程,以及傳送訊息和接受訊息。那麼在linux系統中,核心中已經將這些協議實現,現在我們一起看看linux下套接字程式設計的API。

TCP伺服器端

1. 建立套接字

 #include <sys/socket.h>
 int socket(int family,int type,int
protocol);         返回:非負描述字---成功   -1---失敗

第一個引數指明瞭協議簇,目前支援5種協議簇,最常用的有AF_INET(IPv4協議)和AF_INET6(IPv6協議);第二個引數指明套介面型別,有三種類型可選:SOCK_STREAM(位元組流套介面)、SOCK_DGRAM(資料報套介面)和SOCK_RAW(原始套介面);如果套介面型別不是原始套介面,那麼第三個引數就為0。

2.繫結套接字
把一個套接字地址(本機IP和埠號)繫結到建立的套接字上。繫結套接字時可以選擇指定IP地址和埠,也可以不指定。通配的IP地址用INADDR_ANY表示,通配的埠用0表示,通配的情況下由核心為其指定相應的IP地址和埠號。
對於客戶端可以繫結套接字,但是一般不需要,因為客戶端的埠號只是臨時的,由核心來分配更合理。但是對伺服器而言,一般要使用知名埠號,如果不進行繫結,客戶端不知道目的埠號,連線不能完成。
這裡寫圖片描述


通配地址實現:htonl(INADDR_ANY)
通配地址,核心將等到套接字已連線TCP或已經發出資料報(UDP)時才指定。

#include <sys/socket.h>  
int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen);
 返回:0---成功   -1---失敗 

3.監聽
socket建立的套接字是主動套接字,呼叫listen後變成監聽套接字。TCP狀態有CLOSE躍遷到LISTEN狀態。
backlog是已完成佇列和未完成佇列大小之和,對於監聽套接字有兩個佇列,一個是未完成佇列,一個是已完成佇列。

  • 未完成佇列:客戶端傳送一個SYN包,伺服器收到後變成SYN_RCVD狀態,這樣的套接字被加入到未完成佇列中。
  • 已完成佇列:TCP已經完成了3次握手後,將這個套接字加入到已完成佇列,套接字處於ESTABLISHED狀態。
    這裡寫圖片描述

下圖中可以看出,TCP的三次握手是在呼叫connect函式時完成的,伺服器端沒有呼叫函式,但是必須有套接字在某個埠監聽,不然會返回客戶端RST,終止連線。
這裡寫圖片描述

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

呼叫listen函式後的套接字稱為監聽套接字。

4.accept函式
accept函式從已完成連線的佇列中取走一個套接字,如果該佇列為空,則accept函式阻塞。accept函式的返回值稱為已連線套接字,已連線的套接字就建立一個完整的TCP連線,源IP地址,源埠號,目的IP地址,目的埠號都是唯一確定了。

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

5.資料傳輸

  • write和read函式:當伺服器和客戶端的連線建立起來後,就可以進行資料傳輸了,伺服器和客戶端用各自的套接字描述符進行讀/寫操作。因為套接字描述符也是一種檔案描述符,所以可以用檔案讀/寫函式write()和read()進行接收和傳送操作。

write()函式用於資料的傳送

#include <unistd.h>         
 int write(int sockfd, char *buf, int len); 
  回:非負---成功   -1---失敗

引數sockfd是套接字描述符,對於伺服器是accept()函式返回的已連線套接字描述符,對於客戶端是呼叫socket()函式返回的套接字描述符;引數buf是指向一個用於傳送資訊的資料緩衝區;len指明傳送資料緩衝區的大小。

read()函式用於資料的接收

#include <unistd.h>         
 int read(int sockfd, char *buf, intlen);  
  回:非負---成功   -1---失敗

引數sockfd是套接字描述符,對於伺服器是accept()函式返回的已連線套接字描述符,對於客戶端是呼叫socket()函式返回的套接字描述符;引數buf是指向一個用於接收資訊的資料緩衝區;len指明接收資料緩衝區的大小。

  • send和recv函式:TCP套接字提供了send()和recv()函式,用來發送和接收操作。這兩個函式與write()和read()函式很相似,只是多了一個附加的引數。
    (1)send()函式用於資料的傳送。
#include <sys/types.h>
#include < sys/socket.h >         
ssize_t send(int sockfd, const void *buf, size_t len, int flags);  
回:返回寫出的位元組數---成功   -1---失敗

前3個引數與write()相同,引數flags是傳輸控制標誌。
(2)recv()函式用於資料的傳送。

#include <sys/types.h>
#include < sys/socket.h >         
ssize_t recv(int sockfd, void *buf, size_t len, int flags); 
回:返回讀入的位元組數---成功   -1---失敗

前3個引數與read()相同,引數flags是傳輸控制標誌。

6.關閉套接字
close函式關閉套接字

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

TCP客戶端

1.建立套接字
2.連線伺服器
TCP用connect函式來建立與TCP伺服器的連線。

 #include <sys/socket.h>      
 int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);  
 返回:0---成功   -1---失敗

客戶端傳送的SYN包可能會遇到失敗,可能有以下幾種情況:
1. 如果客戶端沒有收到SYN的響應包,根據TCP的超時重發機制進行重發。75秒後還沒收到,就返回錯誤。
2. 如果目的主機沒有監聽目的埠號,就會返回一個RST的分節,客戶端收到RST後立刻返回錯誤。
3. 如果SYN在中間路由遇到目的不可達,客戶端收到ICMP報文,客戶端儲存這個報文資訊,並採用第一種情況方案解決,也就是重發。

3.收發資料
4.關閉套接字

這裡寫圖片描述

TCP聊天室服務端程式

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>

#define BUFLEN 100

const char* IP = "127.0.0.1";
const unsigned int SERV_PORT = 7777;
void Chat(int sockfd);

int main(int argc, char *argv[])
{
    int listenfd, connectfd;
    struct sockaddr_in s_addr, c_addr;
    char buf[BUFLEN];
    unsigned int port, listnum;
    pid_t childpid;
    socklen_t len;

    /*建立socket*/
    if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        perror("socket");
        exit(errno);
    }
    /*設定伺服器埠*/    
    port = SERV_PORT;

    /*設定偵聽佇列長度*/
    listnum = 5;

    /*設定伺服器ip*/
    bzero(&s_addr, sizeof(s_addr));
    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(port);
//  s_addr.sin_addr.s_addr = inet_aton(IP, &s_addr.sin_addr);
    s_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    /*把地址和埠幫定到套接字上*/
    if((bind(listenfd, (struct sockaddr*) &s_addr,sizeof(struct sockaddr))) == -1){
        perror("bind");
        exit(errno);
    }
    /*偵聽本地埠*/
    if(listen(listenfd, listnum) == -1){
        perror("listen");
        exit(errno);    
    }
    while(1){
        printf("*****************server start***************\n");
        len = sizeof(struct sockaddr);
        if((connectfd = accept(listenfd, (struct sockaddr*) &c_addr, &len)) == -1){
            perror("accept");        
            exit(errno);
        }
        else
        {
            printf("connected with client, IP is: %s, PORT is: %d\n", inet_ntoa(c_addr.sin_addr), ntohs(c_addr.sin_port));
        }

        //建立子程序
        if((childpid = fork()) == 0)
        {
            Chat(connectfd);

            /*關閉已連線套接字*/
            close(connectfd);
            /*是否退出伺服器*/
            printf("exit?:y->yes;n->no ");
            bzero(buf, BUFLEN);
            fgets(buf,BUFLEN, stdin);
            if(!strncasecmp(buf,"y",1)){
                printf("server stop\n");
                break;
            }
            //退出子程序
            exit(0);
        }
    }
    /*關閉監聽的套接字*/
    close(listenfd);
    return 0;
}

void Chat(int sockfd)
{
    socklen_t len;
    char buf[BUFLEN];

    while(1)
    {
        _retry:
            /******傳送訊息*******/
            bzero(buf,BUFLEN);
            printf("enter your words:");
            /*fgets函式:從流中讀取BUFLEN-1個字元*/
            fgets(buf,BUFLEN,stdin);
            /*打印發送的訊息*/
            //fputs(buf,stdout);
            if(!strncasecmp(buf,"quit",4))
            {
                printf("server stop\n");
                break;
            }

            /*如果輸入的字串只有"\n",即回車,那麼請重新輸入*/
            if(!strncmp(buf,"\n",1))
            {
                goto _retry;
            }    

            /*如果buf中含有'\n',那麼要用strlen(buf)-1,去掉'\n'*/            
            if(strchr(buf,'\n'))
            {
                len = send(sockfd, buf,strlen(buf)-1,0);
            }
            /*如果buf中沒有'\n',則用buf的真正長度strlen(buf)*/    
            else
            {
                len = send(sockfd,buf,strlen(buf),0);
            }

            if(len > 0)
                printf("send successful\n");            
            else{
                printf("send failed\n");
                break;            
            }

            /******接收訊息*******/
            bzero(buf,BUFLEN);
            len = recv(sockfd,buf,BUFLEN,0);
            if(len > 0)
                printf("receive massage:%s\n",buf);
            else
            {
                if(len < 0 )
                    printf("receive failed\n");
                else//伺服器呼叫close函式後,系統阻塞函式呼叫,返回0
                    printf("client stop\n");
                break;        
            }
    }
}

TCP聊天室客戶端程式

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>

#define BUFLEN 100

const char* IP = "127.0.0.1";
const int SERV_PORT = 7777;

int main(int argc, char *argv[])
{
    int sockfd;
    struct sockaddr_in s_addr;
    socklen_t len;
    unsigned int port;
    char buf[BUFLEN];    

    /*建立socket*/
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        perror("socket");
        exit(errno);
    }

    /*設定伺服器埠*/    
    port = SERV_PORT;

    /*設定伺服器ip*/
    bzero(&s_addr, sizeof(s_addr));
    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(port);

    if(inet_aton(IP, (struct in_addr*)&s_addr.sin_addr.s_addr) == 0){
        perror("IP error");
        exit(errno);
    }

    /*開始連線伺服器*/    
    if(connect(sockfd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr)) == -1){
        perror("connect");
        exit(errno);
    }else
        printf("*****************client start***************\n");

    while(1){
        /******接收訊息*******/
        bzero(buf,BUFLEN);
        len = recv(sockfd,buf,BUFLEN,0);
        if(len > 0)
            printf("receive massage:%s\n",buf);
        else{
            if(len < 0 )
                printf("receive failed\n");
            else
                printf("server stop\n");
            break;    
        }
    _retry:    
        /******傳送訊息*******/    
        bzero(buf,BUFLEN);
        printf("enter your words:");
        /*fgets函式:從流中讀取BUFLEN-1個字元*/
        fgets(buf,BUFLEN,stdin);
        /*打印發送的訊息*/
        //fputs(buf,stdout);
        if(!strncasecmp(buf,"quit",4)){
            printf("client stop\n");
            break;
        }
        /*如果輸入的字串只有"\n",即回車,那麼請重新輸入*/
        if(!strncmp(buf,"\n",1)){

            goto _retry;
        }
        /*如果buf中含有'\n',那麼要用strlen(buf)-1,去掉'\n'*/    
        if(strchr(buf,'\n'))
            len = send(sockfd,buf,strlen(buf)-1,0);
        /*如果buf中沒有'\n',則用buf的真正長度strlen(buf)*/    
        else
            len = send(sockfd,buf,strlen(buf),0);
        if(len > 0)
            printf("send successful\n");            
        else{
            printf("send failed\n");
            break;            
        }
    }
    /*關閉連線*/
    close(sockfd);

    return 0;
}