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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章