Socket網路程式設計(1):
Linux OS使用Socket的觀念來設計網路程式,所以在介紹UDP,TCP網路程式設計前,先了解一下Socket的基本觀念。
"Socket"有時又稱為[接口],是一種可做雙向資料傳輸的通道,可以將Socket想像成一種裝置,Linux程序可經由此裝置與本地端或是遠端的程序溝通。
本次要介紹的UDP及TCP的網路程式主要是用到Internet-domain socket,如果我們能了解Internet-domain socket
結構的定義與
相關函數的意義,就能寫出UDP及TCP的網路程式。
結構與相關函數:
在設計網路程式之前,先說明Internet-domain socket的定址結構,此結構可以用來儲存IP位址、通訊口等訊息。
[sockaddr_in]定址結構:
struct sockaddr_in{
sa_family_t sin_family; /*AF_INET*/
unsigned short int sin_port; /*port number*/
struct in_addr sin_addr; /*Internet address*/
};
sin_family:是用來說明socket所使用的定址模式,在此必須設為[AF_INET],表示是Internet- domain socket。
sin_port:用來表示TCP/IP的port number,設定sin_port必須使用htons函數做位元排序的動作, 會在之後說明。
sin_addr:用來表示IP位址,結構如下。
struct in_addr{
unsigned long int s_addr;/*儲存IP位址*/
};
[inet_addr()函數]:
inet_addr()函數可用來將[xxx.xxx.xxx.xxx]格式的IP位址轉換成ˇ32-bit unsigned integer。
[格式]
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_addr(const char *string);
其中[string]為一個IP字串,他的格式為[XXX.XXX.XXX.XXX]
位元排序函數:
在設定IPv4 Socket定址結構時,還有一點需要注意的是port number 的設置。
假如我們要設定IP位址[192.168.1.10],port number為[8000],我們會做如下設定
struct sockaddr_in adr_srvr;
adr_srvr.sin_addr.s_addr = inet_addr("192.168.10");
adr_srvr.sin_port = htons(8000);
Socket():
要完成TCP及UDP網路程式設計,需要呼叫許多的SOCKET函數,來幫助我們撰寫網路程式,不論是使用TCP及UDP作為傳輸協定,要透過SOCKET做資料傳輸,首要的工作就是建立SOCKET,要建立SOCKET就可使用socket()函數。
[Socket格式]
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:網域,只要設定為[AF_INET]即可,表示使用Internet協定。
type:連結的型態,若設定值為[SOCK_STREAM]表示為TCP傳輸層協定,若設定值為 [SOCK_DGRAM]表示為UDP傳輸層協定。
protocol:通訊協定,一般設為[0],表示自動選擇。
Bind():
在通訊協定中我們可以使用Bind()函數將IPv4 socket定址結構,連結到我們所建立的socket,如此一來每當有封包到達網路介面時,Linux核心就會將這個封包導向所連結的socket。
[Bind格式]
#include<sys/socket.h>
int bind (int sockfd, const struct sockaddr *my_addr, size_t adr_len);
sockfd: socket函數執行後所傳回的socket描述子
my_addr:指向struct sockaddr_in結構的指標,用來存放欲連結的IPv4定址結構
adr_len: struct sockaddr_in結構的長度,可將其指定為sizeof(struct sockaddr_in)
Listen():
建立好socket並做好bind後,在server端,我們可以使用listen()函數來通知Linux核心,將socket設為[listening socket],等待client端的連線要求。
[Listen格式]
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd: socket函數執行後所傳回的socket描述子
backlog:指定最大連線數量,通常設為5
Accept():
當server端建立好listening socket並接收到client端的連線請求時,就會將連線請求放入連線佇列中,接著server端必須呼叫accept()函數,來處理並接受佇列中的連線請求。
[Accept格式]
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);
sockfd: socket函數執行後所傳回的socket描述子
addr:指向struct sockaddr_in結構的指標,用來存放client端的IP address
addrlen:存放client IP address變數的長度,初值應為sizeof(struct sockaddr_in),accept()執行成 功後會存回實際的client IP address 長度
Connect():
在TCP中,當client端建立好了socket後可以呼叫connect()函數向server端要求建立連線,建立完畢後才能互傳資料。
[Connect格式]
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
sockfd: socket函數執行後所傳回的socket描述子
serv_addr:指向struct sockaddr_in結構的指標,用來存放server端的IP address
addrlen: struct sockaddr_in結構的長度,可指定為sizeof(struct sockaddr_in)
Close():
我們可以呼叫close()函數來中止連線。
[Close格式]
#include <sys/socket.h>
int close(int sockfd);
sockfd: socket函數執行後所傳回的socket描述子
TCP程式設計流程:
在設計網路程式我們會分為客戶端與伺服端,綜合以上的說明,我們可以將Linux TCP程式設計流程,以下圖表示。
TCP輸出入函數:
當我們在server端建立了socket做好bind,產生listening socket,接受client端連線要求後,server端便可以與client端彼此傳遞資料,要傳遞資料,需要使用輸出入函數,TCP socket的輸出入函數有許多種,最基礎的便是read()、write()函數。
Read():
read()函數會從已開啟的socket讀取資料。
[Read格式]
#include <sys/socket.h>
int read(int sockfd, char *buf, int len);
sockfd: socket函數執行後所傳回的socket描述子
buf:指向字元暫存器的指標,用來儲存讀取到的資料
len:欲讀取的字元長度
Write():
write()函數可以將資料寫入已開啟的socket。
[Read格式]
#include <sys/socket.h>
int write(int sockfd, char *buf, int len);
sockfd: socket函數執行後所傳回的socket描述子
buf:指向字元暫存器的指標,用來儲存欲寫入的資料
len:欲寫入的字元長度
Recv():
recv()函數可以經由socket來接收資料。
[Recv格式]
#include <sys/types.h>
#include <sys/socket.h>
int recv(int s, void *buf, int len, unsigned int flags);
[note]:recv()用來接收遠端socket傳來的資料,並把資料儲存在buf指向的記憶體中,len為可接 收的最大長度,flags一般設為0
Send():
send()函數可以經由socket來傳送資料。
[Send格式]
#include <sys/types.h>
#include <sys/socket.h>
int send(int s, const void *msg, int len, unsigned int flags);
[note]:send()函數可以將msg指向的資料,經由socket傳送給遠端的主機,len則為傳送資料最大 長度,flags一般設為0。
--------------------------------------------------------------------------------------------------------------------------
由上述介紹後,我們來實作TCP通訊協定的server程式與client程式
動作要求:
1.server端會建立socket,連結socket,等待client端連線
2.client端會建立socket,與server連線,若成功,將str字串傳給server,str字串內容為[品號 品 名 售價]
3.server端若接受client端的連線,會讀取字串並解析內容,算出售價的含稅價,再將處理後的 字串回傳給client
4.client端收到server端的訊息,會將訊息顯示在螢幕上
程式:server端
/* server.c */
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
int port = 8000;
int main(void)
{
struct sockaddr_in sin;
struct sockaddr_in pin;
int mysock;
int tempsock;
int addrsize;
char str[100],str1[20],str2[20],str3[20];
char buf[100];
int i, len1, len2;
float c;
mysock = socket(AF_INET, SOCK_STREAM, 0);
if (mysock == -1) {
perror("call to socket");
exit(1);
}
bzero(&sin, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_ANY);
sin.sin_port = htons(port);
if (bind(mysock, (struct sockaddr *)&sin, sizeof(sin)) == -1) {
perror("call to bind");
exit(1);
}
if (listen(mysock, 20) == -1) {
perror("call to listen");
exit(1);
}
printf("Accepting connections ...\n");
while(1) {
tempsock = accept(mysock, (struct sockaddr *)&pin, &addrsize);
if (tempsock == -1){
perror("call to accept");
exit(1);
}
len1=recv(tempsock, str, 100, 0);
printf("\n收到字元數: %d\n",len1);
str[len1]=0;
printf("received from client: %s\n", str);
if (len1 > 0) {
strcpy(str1,strtok(str," "));
printf("第 1 個字串為: %s\n",str1);
strcpy(str2,strtok(NULL," "));
printf("第 2 個字串為: %s\n",str2);
strcpy(str3,strtok(NULL," "));
printf("第 3 個字串為: %s\n",str3);
c=atof(str3)*1.05;
sprintf(buf,"品號為 %s\n品名為 %s\n含稅價為: %.2f\n",str1, str2, c);
}
len2 = strlen(buf);
if (send(tempsock, buf, len2, 0) == -1) {
perror("call to send");
exit(1);
}
close(tempsock);
}
return 0;
}
程式:client端
/* client.c */
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
int port = 8000;
int main(int argc, char *argv[])
{
struct sockaddr_in pin;
int mysock;
char buf[8192];
char *str="A001 電視機 20000.00 ";
if (argc < 2) {
printf("使用方法: client 字串\n");
printf("使用預設字串\n");
} else {
str=argv[1];
}
bzero(&pin, sizeof(pin));
pin.sin_family = AF_INET;
pin.sin_addr.s_addr = inet_addr("192.168.1.20");
pin.sin_port = htons(port);
mysock = socket(AF_INET, SOCK_STREAM, 0);
if (mysock == -1) {
perror("call to socket");
exit(1);
}
if (connect(mysock, (void *)&pin, sizeof(pin)) == -1) {
perror("call to connect");
exit(1);
}
printf("Sending message %s to server ...\n", str);
if (send(mysock, str, strlen(str), 0) == -1) {
perror("Error in send\n");
exit(1);
}
if (recv(mysock, buf, 8192, 0) == -1) {
perror("Error in receiving\n");
exit(1);
}
printf("\nResponse from server: \n\n%s\n", buf);
close(mysock);
return 0;
}
--------------------------------------------------------------------------------------------------------------------------
sin.sin_addr.s_addr = htonl(INADDR_ANY):
讓Server端的socket可以接收同一個網域內所有的IP位址,INADDR_ANY可視為[萬用位 址],同樣地,若要讓socket接收同區網的所有port number,指令如下:
sin.sin_port = htons(0);
指令列引數:
int main(int argc, char *argv[]){....}
此為C語言的一項功能,此功能可以讓程式在執行時,接受由使用者輸入一些字串,作為主程式的參數,其中第一個引數[argc]會儲存使用者輸入的字串個數,而第二個引數[argvp[]]為字串指標陣列,指向使用者所輸入的字串內容。
例如,當我們執行client程式可以輸入以下字串:
./client "A002 音響 15000.00"
此時,argc的值為2,argv[0]的內容為client,argv[1]的內容為"A002 音響 15000.00"。