1. 程式人生 > >linux網路程式設計之用socket實現簡單客戶端和服務端的通訊(基於TCP)

linux網路程式設計之用socket實現簡單客戶端和服務端的通訊(基於TCP)

一、介紹基於TCP協議通過socket實現網路程式設計常用API

1、讀者如果不是很熟悉,可以先看我之前寫的幾篇部落格,有socket,地址結構的理解,更加方便讀者理解

地址分別是:

2、socket(TCP)程式設計API簡介

1)、socket

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

 socket()開啟一個網路通訊埠,如果成功的話,就像open()一樣返回一個檔案描述符,應用程式可以像讀寫檔案一樣用read/write在網路上收發資料,如果socket()調用出錯則返回-1。對於IPv4,family引數指定為AF_INET。對於TCP協議,type引數指定為SOCK_STREAM,表示面向流的傳輸協議。如果是UDP協議,則type引數指定SOCK_DGRAM,表示面向資料報的傳輸協議。protocol引數的介紹從略,指定為0即可。

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);


 伺服器程式所監聽的網路地址和埠號通常是固定不變的,客戶端程式得知伺服器程式的地址和埠號後就可以向伺服器發起連線,因此伺服器需要呼叫bind繫結一個固定的網路地址和埠號,bind()成功返回0,失敗返回-1。  bind()的作用是將引數sockfd和myaddr繫結在一起,使sockfd這個用於網路通訊的檔案描述符
監聽myaddr所描述的地址和埠號。前面講過,struct sockaddr *是一個通用指標型別,myaddr參
數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需要第三個引數
addrlen指定結構體的長度。我們的程式中對myaddr引數是這樣初始化的:

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

首先將整個結構體清零,然後設定地址型別為AF_INET,網路地址為INADDR_ANY,這個巨集表示本地的任意IP地址,因為伺服器可能有多個網絡卡,每個網絡卡也可能繫結多個IP地址,這樣設定可以在所有的IP地址上監聽,直到與某個客戶端建立了連線時才確定下來到底用哪個IP地址,埠號為SERV_PORT,我們定義為1234。

2)、listen

int listen(int sockfd, int backlog);


 典型的伺服器程式可以同時服務於多個客戶端,當有客戶端發起連線時,伺服器呼叫的accept()返回並接受這個連線,如果有大量的客戶端發起連線而伺服器來不及處理,尚未accept的客戶端就處於連線等待狀態,listen()宣告sockfd處於監聽狀態,並且最多允許有backlog個客戶端處於連線待狀態,如果接收到更多的連線請求就忽略。listen()成功返回0,失敗返回-1。

3)、accept

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

三方握手完成後,伺服器呼叫accept()接受連線,如果伺服器呼叫accept()時還沒有客戶端的連線請求,就阻塞等待直到有客戶端連線上來。cliaddr是一個傳出引數,accept()返回時傳出客戶端的地址和埠號。addrlen引數是一個傳入傳出引數(value-result argument),傳入的是呼叫者提供的緩衝區cliaddr的長度以避免緩衝區溢位問題,傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿呼叫者提供的緩衝區)。如果給cliaddr引數傳NULL,表示不關心客戶端的地址。
我們的伺服器程式結構是這樣的:
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}


整個是一個while死迴圈,每次迴圈處理一個客戶端連線。由於cliaddr_len是傳入傳出引數,每次呼叫accept()之前應該重新賦初值。accept()的引數listenfd是先前的監聽檔案描述符,而accept()的返回值是另外一個檔案描述符connfd,之後與客戶端之間就通過這個connfd通訊,最後關閉connfd斷開連線,而不關閉listenfd,再次回到迴圈開頭listenfd仍然用作accept的引數。accept()成功返回一個檔案描述符,出錯返回-1。由於客戶端不需要固定的埠號,因此不必呼叫bind(),客戶端的埠號由核心自動分配。注意,客戶端不是不允許呼叫bind(),只是沒有必要呼叫bind()固定一個埠號,伺服器也不是必須呼叫bind(),但如果伺服器不呼叫bind(),核心會自動給伺服器分配監聽埠,每次啟動伺服器時埠號都不一樣,客戶端要連線伺服器就會遇到麻煩。

4)、connect

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


客戶端需要呼叫connect()連線伺服器,connect和bind的引數形式一致,區別在於bind的引數是自己的地址,而connect的引數是對方的地址。connect()成功返回0,出錯返回-1。

5)、Write函式

    Ssize_t write(int fd,const void *buf,size_t nbytes);


    Write函式將buf中的nbytes位元組內容寫入到檔案描述符中,成功返回寫的位元組數,失敗返回-1.並設定errno變數。在網路程式中,當我們向套接字檔案描述舒服寫資料時有兩種可能:

    1、write的返回值大於0,表示寫了部分資料或者是全部的資料,這樣用一個while迴圈不斷的寫入資料,但是迴圈過程中的buf引數和nbytes引數是我們自己來更新的,也就是說,網路程式設計中寫函式是不負責將全部資料寫完之後再返回的,說不定中途就返回了!

    2、返回值小於0,此時出錯了,需要根據錯誤型別進行相應的處理。

    如果錯誤是EINTR表示在寫的時候出現了中斷錯誤,如果是EPIPE表示網路連接出現了問題。

6)、Read函式

    Ssize_t read(int fd,void *buf,size_t nbyte)

    Read函式是負責從fd中讀取內容,當讀取成功時,read返回實際讀取到的位元組數,如果返回值是0,表示已經讀取到檔案的結束了,小於0表示是讀取錯誤。

    如果錯誤是EINTR表示在寫的時候出現了中斷錯誤,如果是EPIPE表示網路連接出現了問題。
    

7)、Recv函式和send函式


    Recv函式和read函式提供了read和write函式一樣的功能,不同的是他們提供了四個引數。

    Int recv(int fd,void *buf,int len,int flags)

    Int send(int fd,void *buf,int len,int flags)


    前面的三個引數和read、write函式是一樣的。第四個引數可以是0或者是一下組合:

    MSG_DONTROUTE:不查詢表

    是send函式使用的標誌,這個標誌告訴IP,目的主機在本地網路上,沒有必要查詢表,這個標誌一般用在網路診斷和路由程式裡面。

    MSG_OOB:接受或者發生帶外資料

    表示可以接收和傳送帶外資料。

    MSG_PEEK:檢視資料,並不從系統緩衝區移走資料

    是recv函式使用的標誌,表示只是從系統緩衝區中讀取內容,而不清楚系統緩衝區的內容。這樣在下次讀取的時候,依然是一樣的內容,一般在有過個程序讀寫資料的時候使用這個標誌。

    MSG_WAITALL:等待所有資料

    是recv函式的使用標誌,表示等到所有的資訊到達時才返回,使用這個標誌的時候,recv返回一直阻塞,直到指定的條件滿足時,或者是發生了錯誤。

 8) 、close的定義如下:

#include<unistd.h>

int close(int fd);

關閉讀寫。

成功則返回0,錯誤返回-1,錯誤碼errno:EBADF表示fd不是一個有效描述符;EINTR表示close函式被訊號中斷;EIO表示一個IO錯誤。

2、客戶端和服務端的通訊圖

3、實現現簡單客戶端和服務端的通訊(基於TCP)

         這是實現的是客戶端寫資料,服務端讀取,然後把讀取的資料寫到客戶端顯示

1、服務端socket1.c的程式碼如下

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

#define PORT  2345
#define MAXSIZE 1024

int main(int argc, char *argv[])
{
    int sockfd, newsockfd;
    //定義服務端套介面資料結構
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    int sin_zise, portnumber;
    //傳送資料緩衝區
    char buf[MAXSIZE];
    //定義客戶端套介面資料結構
    int addr_len = sizeof(struct sockaddr_in);
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        fprintf(stderr, "create socket failed\n");
	exit(EXIT_FAILURE);
    }
    puts("create socket success");
    printf("sockfd is %d\n", sockfd);
    //清空表示地址的結構體變數
    bzero(&server_addr,  sizeof(struct sockaddr_in));
    //設定addr的成員變數資訊
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    //設定ip為本機IP
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  
    if (bind(sockfd, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr)) < 0)
    {
        fprintf(stderr, "bind failed \n");
	exit(EXIT_FAILURE);
    } 
    puts("bind success\n");
    if (listen(sockfd, 10) < 0)
    {
        perror("listen fail\n");
	exit(EXIT_FAILURE);
    }
    puts("listen success\n");
    int  sin_size = sizeof(struct sockaddr_in);
    printf("sin_size is %d\n", sin_size);
    if ((newsockfd = accept(sockfd, (struct sockaddr *)(&client_addr), &sin_size)) < 0)
    {
        perror("accept error");
        exit(EXIT_FAILURE);
    } 
    printf("accepted a new connetction\n");
    printf("new socket id is %d\n", newsockfd);
    printf("Accept clent ip is %s\n", inet_ntoa(client_addr.sin_addr));
    printf("Connect successful please input message\n");
    char sendbuf[1024];
    char mybuf[1024];
    while (1)
    {
  	 int len = recv(newsockfd, buf, sizeof(buf), 0);
         if (strcmp(buf, "exit\n") == 0)
             break;
          fputs(buf, stdout);
          send(newsockfd, buf, len, 0);
          memset(sendbuf, 0 ,sizeof(sendbuf));
	  memset(buf, 0, sizeof(buf));
    }
    close(newsockfd); 
    close(sockfd);
    puts("exit success");
    exit(EXIT_SUCCESS);
    return 0;
}

2、客戶端socket3.c的程式碼如下

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

#define PORT 2345

int count = 1;

int main()
{
   int sockfd;
   char buffer[2014];
   struct sockaddr_in server_addr;
   struct hostent *host;
   int nbytes;
   if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
   {
      fprintf(stderr, "Socket Error is %s\n", strerror(errno));
      exit(EXIT_FAILURE);
   }
   bzero(&server_addr, sizeof(server_addr));
   server_addr.sin_family = AF_INET;
   server_addr.sin_port = htons(PORT);
   server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
   //客戶端發出請求
  
   if (connect(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) == -1)
   {
      fprintf(stderr, "Connect failed\n");
      exit(EXIT_FAILURE);
   }
   char sendbuf[1024];
   char recvbuf[2014];
   while (1) 
   { 
     fgets(sendbuf, sizeof(sendbuf), stdin);
     send(sockfd, sendbuf, strlen(sendbuf), 0);
   if (strcmp(sendbuf, "exit\n") == 0)
      break;
      recv(sockfd, recvbuf, sizeof(recvbuf), 0);
      fputs(recvbuf, stdout);
      memset(sendbuf, 0, sizeof(sendbuf));
      memset(recvbuf, 0, sizeof(recvbuf));
   }  
   close(sockfd);
   exit(EXIT_SUCCESS);
   return 0;
}

4、執行結果

5、總結

服務端:socekt -> bind-> listen->accept

客戶端:socket->connect

tcp是面向連線的,安全的,無重複的,排列有序的。