2009年2月3日 星期二

socket - tcp

當client與server連接之後,
client可以連續傳訊息給server,
但是,server不能連續傳訊息給client,
像是server要傳檔案給client,分割好幾份,
則在server傳給client每一份之間,client必需傳一個訊息給server,
如此一來,server才可以傳下一份分割給client
下面我用gcc寫了一個非常簡單的傳輸檔案的程式:
server.c

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

//==========================define variable===============================
#define PORT 9734
#define FILEBUFFERSIZE 1024
//========================================================================

//==========================global variable===============================
//========================================================================

//==========================function phototypes===========================
//========================================================================

int main(int argc,char *argv[])
{
  int listenfd,connfd;
  // 在c語言中,要用到struct時,前面必需要加上struct,在c++中則不需要
  struct sockaddr_in server_address;
  struct sockaddr_in client_address;
  int server_len,client_len;
  char temp[FILEBUFFERSIZE];
  int ReadByte,WriteByte;
  char end[]="#end#"; // 用來判斷是否傳送檔案結束
  FILE *fp;

  if (argc!=2) {
    printf("Insrtuction error!!\n");
    printf("./server 'file'\n");
    exit(1);
  }
  if ((fp=fopen(argv[1],"rb"))==NULL) {
    fputs("open file error\n",stderr);
    exit(1);
  }
  // divide - test
  while((ReadByte=fread(temp,sizeof(char),FILEBUFFERSIZE,fp))>0) {
    printf("%d\n",ReadByte);
    memset(temp,0,FILEBUFFERSIZE);
  }
  // divide - test

  // 1.建立一個新的通訊端:socket(int domain,int type,int protocol),並回傳參數值
  listenfd = socket(AF_INET, SOCK_STREAM, 0);
  // 2.1.設定為Unix network socket,表示可以在兩個不同的電腦之間傳資料
  server_address.sin_family = AF_INET;
  // 2.2.server的ip位址,這裡必需是server的ip位
  // inet_addr(), this function translate IP address into specified format for socket
  server_address.sin_addr.s_addr=INADDR_ANY;
  // server_address.sin_port = 9734;
  // 2.3.轉換成little-end,因為intel是little-end
  server_address.sin_port = htons(PORT);
  // 記錄server端的設定(port,ip,sin_family...)的資料長度
  server_len = sizeof(server_address);
  // 3.Socket 出入口需Binding到TCP address
  if(bind(listenfd,(struct sockaddr*) &server_address, server_len)==-1){
    printf("bind error");
    exit(1);
  }
  // 4.建立一個queue以接收其他程式所送達的連線要求,queue的大小為5
  if (listen(listenfd,5)==-1){
    printf("listening error");
    exit(1);
  }
  // 當產生了一個佇列(queue)後,再來就是要把queue中的連結請求(Connect Request)讀出
  while(1)
  {
    // temp存放要傳送的檔案
    char temp[FILEBUFFERSIZE];
    printf("server waiting \n");
    client_len = sizeof(client_address); // 了解client的設定資料結構長度
    // 5.以accept指令來等待client端送去連線要求(connect request)
    // 由client_address獲得client端的網路位置
    // connfd即由新的connected socket 所存放參數的地方。用來和client 進行資料輸出入
    connfd = accept(listenfd,(struct sockaddr *)&client_address, &client_len);
    if (connfd==-1){
      printf("Error:accept()\n");
      exit(1);
    }
    // 由connfd讀取資料存到temp的位址中,buffer大小為1024 bytes
    read(connfd, &temp, FILEBUFFERSIZE);
    if (printf("ch : %s \n",temp)==-1){
      printf("Read error!\n");
      exit(1);
    }
    else
    {
      if (strncmp(temp,"get",strlen("get"))==0)
      {
        printf("File transmite...\n");
        fp=fopen(argv[1],"rb");
        if (fp==NULL) {
          printf("open file error!!\n");
          exit(1);
        }
        memset(temp,0,sizeof(temp));
        ReadByte=fread(temp,sizeof(char),FILEBUFFERSIZE,fp);
        while(ReadByte>0){
          printf("read %d Byte\n",ReadByte);
          WriteByte=write(connfd,temp,ReadByte);
          printf("transmite %d bytes\n",WriteByte);
          memset(temp,0,sizeof(temp));
          ReadByte=read(connfd,temp,FILEBUFFERSIZE);
          printf("%s\n",temp);
          memset(temp,0,FILEBUFFERSIZE);
          ReadByte=fread(temp,sizeof(char),FILEBUFFERSIZE,fp);
        }

      }
      WriteByte=write(connfd,end,sizeof(end));
      printf("Transmite %d bytes\n",WriteByte);
    }
    // 關掉connected socket
    close(connfd);
  }
  // 關掉listen的socket
  close(listenfd);
  fclose(fp);

  return 0;
}

//==========================function implementation=======================
//========================================================================


client.c

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

//=============================define variable===============================
#define PORT 9734
#define FILEBUFFERSIZE 1024
//===========================================================================

int main(int argc,char *argv[])
{
  int sockfd;
  int ReadByte;
  struct sockaddr_in address;
  int client_len,result;
  FILE *fp;
  char server_addr[15];
  if (argc!=2) {
    fputs("./client get",stderr);
    exit(1);
  }
  if((fp=fopen("copy","wb"))==NULL) {
    printf("create error\n");
    exit(1);
  }
  fclose(fp);
  printf("Please input server's ip:");
  scanf("%s",server_addr);
  // 建立一個socket
  sockfd = socket(AF_INET, SOCK_STREAM, 0);   address.sin_family = AF_INET;
  // 以下存放的必需是server的ip位址
  address.sin_addr.s_addr=inet_addr(server_addr);
  address.sin_port = htons(PORT);
  client_len = sizeof(address);
  // Client端則呼叫connect()功能,要求與Server主機建立連接通道
  result = connect(sockfd, (struct sockaddr *)&address,client_len);
  if(result == -1)  // 若傳回-1則表示連線失敗
  {
    perror("oops: client1");
    fclose(fp);
    exit(1) ;
  }
  // 傳送資料到server端
  if (write( sockfd, &(*argv[1]) , FILEBUFFERSIZE)==-1){
    printf("write error\n");
    fclose(fp);
    exit(1);
  }
  else {
    // 接收檔案
    char temp[FILEBUFFERSIZE];
    memset(temp,0,FILEBUFFERSIZE);
    ReadByte=read(sockfd,temp,FILEBUFFERSIZE);
    printf("get the %d bytes\n",ReadByte);
    fp = fopen("copy","wb");
    if (fp==NULL) {
      printf("open file error!!\n");
      exit(1);
    }
    else {
      while(ReadByte>0) {
        if (strncmp(temp,"#end#",strlen("#end#"))==0) {
          printf("The connect is over\n");
          break;
        }
        fwrite(temp,sizeof(char),ReadByte,fp);
        write(sockfd,"I got\n",sizeof("I got\n"));
        memset(temp,0,FILEBUFFERSIZE);
        ReadByte=read(sockfd,temp,FILEBUFFERSIZE);
        printf("I got the %d bytes\n",ReadByte);
      }
      fclose(fp);
    }
    // 當client接收到資料後,必需要write資料到server才能再read資料!,這樣才叫被動啊!!
  }
  close( sockfd); // 關掉socket介面

  return 0;
}

執行:

[user@host ~]$ ./server source
[user@host ~]$ ./client get

就會在目錄下出現一個copy的檔案

這裡用到的新函數
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
由FILE指標指到的檔案讀取單位為size byte,且個數上限為count的資料,並存到ptr所指到的指標位址空間,回傳值為實際讀取到的byte數

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
由FILE指標指到的檔案寫入單位為size byte,且個數上限為count的資料,並存到ptr所指到的指標位址空間,回傳值為實際寫入到的byte數

int socket(int domain, int type, int protocol)
– Doman : AF_UNIX UNIX files system sockets
AF_INET → UNIX network sockets
AF_ISO → ISO standard protocols
AF_NS → Xerox Network Systems protocols
AF_IPX → Novell IPX protocol
– Type : SOCK_STREAM(TCP),SOCK_DGRAM(UDP)
– Protocol : default 0

int bind(int socket, const struct socket * address, size_t address_len);
socket : return value from socket function
– 利用socket函數產生socket後所回傳的參數值
• *address : struct sockaddr_un, struct sockaddr_in
– 對映於通訊端(socket)所宣告的資料結構
• address_len : address length
– 參數為資料結構的大小
當成功後,函式會傳回0以代表成功,若失敗即傳回-1。

建立一個queue以接收其他程式所送達的連線要求(request)
– int listen(int socket, int backlog)
• socket : return value from socket function
– 為所新建立的socket descriptor
• Backlog : Queue length
– 代表所欲建立的queue大小為多少
當成功後,函式會傳回0以代表成功,若失敗即傳回-1。

當產生了一個佇列(queue)後,再來就是要把queue中的連結請求(Connect Request)讀出
• 其函數的原型如下:
– int connect(int socket, const struct sockaddr
*address, size_t address_len)
• socket : return value from socket function
– 為新建立的socket descriptor
• *address : struct sockaddr_un, struct sockaddr_in
– 對映於通訊端(socket)所宣告的資料結構
• address_len : address length
– 為address資料結構的大小

SOCKET accept(SOCKET s,struct sockaddr* addr,int* addrlen);
s:一個設定為listen狀態的socket
sddr:Client端位址資訊,由函式自動產生填入
addrlen:sddr長度,由函式自動產生
回傳值:-1表示錯誤,否則傳回另一個包含Client端資訊的新socket
descriptor,作為傳送資料用
傳進accept()的listen socket本身並沒有辦法作資料的傳輸,所以必須透過accept()產生一個包含通訊協定、Server、Client資訊的新socket,利用他就可以進行資料的傳輸了

int count=send( int sockfd, char *buffer, int buflen, int flags )

–count:傳送資料的大小
–buffer:資料傳送的指標
–buflen:資料長度
–flag:通常設為0
–return為實際傳送的byte數
–Or write
• int write(int sockfd, char * buffer, int buflen);

Int count=recv( int sockfd, char *buffer, int buflen, int flags )
–Count:接收到的資料大小
–buffer:資料接收後儲存位址的指標
–buflen:資料長度
–flag:通常設為0
–return為實際傳送的byte數
–Or read
• int read(int sockfd, char * buffer, int buflen);

int close (int sockfd) ;
用來關閉socket,並且終止TCP連線


參考資料:
Linux socket 編程入門
網路程式規畫
TCP通訊協定的server程式及client程式
socket編程原理
使用Socket完成FTP檔案存取
程式設計範例及習題 Chapter 5 網路程式範例
Socket Programming HOWTO

以後可能會用到:
[C/C++] 解決Socket連續Bind同一個Port的問題
UNIX Network Programming Source Code

沒有留言: