本文的目的在於為初學者提供一個快速的入門指導,用來迅速熟悉用C語言來編寫
Internet 網路應用程式。本文假設讀者已經具備了C語言的基本知識和文法,並且讀者有使用Uinx/Linux的經驗。儘管Uinx/Linux的Socket編程 與在Windows下的有一些不同的地方,但是在此我並不想展開。另外,本文所有的程式都在Red Hat 5.2下編譯通過,並且在 glibc 2.0.7和libc 5.3.12兩種環境下都沒有問題。現在就開始我們的教程吧:)。
對一個程式員而言,sockets和底層的檔案描述符非常類似(可以在sockets裡使用read()和write()函數),儘管建立一個socket比開啟,讀取和寫入一個檔案更為麻煩,但這是由於網路連接比單純的本地硬碟的讀寫複雜的多所造成的。
通常,sockets用來實現客戶機/伺服器對。伺服器的任務是監聽某個特定的連接埠,當接收到用戶端的服務要求時完成相應的服務;客戶機的任務是請求伺服器完成事先設定好的服務。
作為入門級的文章,我們在這裡不會使用所有的socket類型和功能,但是我們會向讀者提供足夠的資訊。現在,就讓我們開始吧。
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
建立一個socket:socket()
你所要學的socket編程的第一件事就是用socket()建立一個socket:
- -------
#include <sys/types.h>
#include <sys/socket.h>
int socket(int af, int type, int protocol)
- ------
'int af'代表地址族或者稱為socket所代表的域,通常有兩個選項:
AF_UNIX - 只在單機上使用。
AF_INET - 可以在單機或其他使用DARPA協議(UDP/TCP/IP)的異種機通訊。
'int type'代表你所使用的連線類型,通常也有兩種情況:
SOCK_STREAM - 用來建立連線導向的sockets,可以進行可靠無誤的的資料傳
輸
SOCK_DGRAM - 用來建立沒有串連的sockets,不能保證資料轉送的可靠性。
在本文中,我們著重使用AF_INET地址族和SOCK_STREAM連線類型。
'int protocol'通常設定為0。這樣的目的是使系統選擇預設的由協議族和連線類型所確定的協議。
這個函數的傳回值是一個檔案描述控制代碼,如果在此期間發生錯誤則返回-1並且設定了相應的errno。
- ------
#include <sys/types.h>
#include <sys/socket.h>
int sockfd /* soon to be socket file descriptor */
sockfd = socket(AF_INET, SOCK_STREAM, 0)
/* error checking here */
- ------
如果執行成功,我們就擁有了一個socket的檔案控制代碼,通過這個控制代碼就可以訪問
Internet了。
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
名字綁定socket: bind()
下一步要完成的就是名字綁定工作了:
- ------
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *name, int namelen)
- ------
在這個函數裡,sockfd是從socket()調用得到的檔案描述控制代碼。name是一個指向
sockaddr類型結構的一個指標。如果地址族被設定為AF_UNIX,這個類型的定義是
如下所示:
- ------
struct sockaddr {
u_short sa_family;
char sa_data[14];
};
- ------
在這個結構種,name.sa_family應當被設定為AF_UNIX。name.sa_data應當包含最
長為14個位元組的檔案名稱,這個檔案名稱用來分配給socket。namelen給出了檔案名稱的
具體長度。
- ------
#include <sys/types.h>
#include <sys.socket.h>
struct sockaddr name;
int sockfd;
name.sa_family = AF_UNIX;
strcpy(name.sa_data, "/tmp/whatever");
sockfd = socket(AF_UNIX, SOCK_STREAM, 0)
/* error checking code here */
bind(sockfd, &name, strlen(name.sa_data) + sizeof(name.sa_family)
/* error checking code here */
- ------
如果調用成功,則傳回值為0,如果調用失敗傳回值為-1,並設定相應的錯誤碼errno。
現在,讓我們在使用另一種結構,它是在使用AF_INET地址族的時候使用的。
- -----
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
- ------
看起來它比前一個大多了,但要掌握它並不十分困難。
- ------
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
int sockfd, port = 23;
struct sockaddr_in my_addr;
if((sockfd=socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("Socket Error, %d\n", errno);
exit(1);
}
my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(port); /* see man htons for more information
*/
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* get our address */
bzero(&(my_addr.sin_zero), 8); /* zero out the rest of the space */
if((bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))
== -1)
{
printf("Bind Error, %d\n", errno);
close(sockfd);
exit(1);
}
- ------
現 在,如果沒有問題的話,我們建立的socket就有一個名字了!相反,如果不成功,它會設定相應的錯誤碼,並使程式退出。這裡需要說明的是,如果你的計 算機不想和別人的電腦串連,那麼完全沒有必要使用bind()。對於連接埠的綁定,在伺服器而言是不合適的,它只應該在客戶機上實現。
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
遠端連線: connect()
這是和別的電腦串連所必須的一步。
- ------
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
- ------
sockfd是我們建立的檔案描述控制代碼,serv_addr是一個sockaddr結構,包含目的的
地址和連接埠號碼,addrlen 被設定為sockaddr結構的大小。
- ------
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define DEST_IP "132.241.5.10"
#define DEST_PORT 23
main()
{
int sockfd;
struct sockaddr_in dest_addr; /* will hold the destination addr */
sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! *
/
dest_addr.sin_family = AF_INET; /* host byte order */
dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
bzero(&(dest_addr.sin_zero), 8); /* zero the rest of the struct */
connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr)
);
/* error checking code here */
/* more code
.
.
.
*/
}
- ------
同樣,connect()在調用返回後,如果傳回值為0則表明成功,如果是1則說明有錯誤,並且同時設定了相應的錯誤碼。由於我們現在不關心具體和那個連接埠串連,所以在上面的常式裡我們沒有調用了bind()函數。
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
監聽: listen()
當我們需要建立一個伺服器的時候,我們需要有一種手段來監聽輸入的請求,而
listen()函數正是提供這個功能。
- ------
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- ------
參數backlog是指一次可以監聽多少個串連
它的調用返回結果和上述的幾個函數是一樣的,這裡就不多說了。
值得一提的是,在這裡,我們需要建立一個綁定,用來接收特定連接埠的服務要求。
- ------
socket(); /* to create out socket file descriptor */
bind(); /* to give our socket a name */
listen(); /* listen for connection */
- ------
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
接受串連: accept()
好了,現在開始實質性的工作了。當有人試圖從我們開啟的連接埠登陸進來時我們應該響應他,這個時候就要用到accept()函數了。
- ------
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, void *addr, int *addrlen);
- ------
函數調用所需的參數都是我們所熟悉的:)
- ------
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define MYPORT 1500 /* the port users will be connecting to */
#define BACKLOG 5 /* how many pending connections queue will hold */
main()
{
int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */
struct sockaddr_in my_addr; /* my address information */
struct sockaddr_in their_addr; /* connector's address information */
int sin_size;
sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! *
/
my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(MYPORT); /* short, network byte order */
my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */
/* did you remember your error checking? */
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
listen(sockfd, BACKLOG);
sin_size = sizeof(struct sockaddr_in);
new_fd = accept(sockfd, &their_addr, &sin_size);
- ------
這裡我們要注意的是:我們用new_fd來完成所有的接收和發送的操作。如果在只有一個串連的情況下你可以關閉原來的sockfd用來防止更多的輸入請求。
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
輸入和輸入的完成: send() and recv()
在我們完成了上述的工作後,最後一步就是傳輸資料了:)。在這裡,我們通過send()和recv()來實現。
- ------
#include <sys/types.h>
#include <sys/socket.h>
int send(int sockfd, const void *msg, int len, int flags);
int recv(int sockfd, void *buf, int len, unsigned int flags);
- ------
send():
sockfd - socket file descriptor
msg - message to send
len - size of message to send
flags - read 'man send' for more info, set it to 0 for now
recv():
sockfd - socket file descriptor
buf - data to receive
len - size of buf
flags - same as flags in send()
send() 常式:
- ------
char *msg = "Hey there people";
int len, send_msg;
/* code to create(), bind(), listen() and accept() */
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
- ------
recv() 常式:
- ------
char *buf;
int len, recv_msg;
/* code to create(), bind(), listen() and accept() */
len = strlen(buf);
recv_msg = recv(sockfd, buf, len, 0);
- ------
如果你使用的連線類型是SOCK_DGRAM,那麼應該使用sendto()和recvfrom()來實現資料轉送。
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
最後一步: close() and shutdown()
當傳輸結束時,應當關閉串連。
- ------
#include <stdio.h>
/* all you code */
close(sockfd);
- ------
更保險的方法是用shutdown()來關閉串連。
- ------
int shutdown(int sockfd, int how)
- ------
參數how的選擇:
1 - 不允許接收更多的資料
2 - 不允許發送更多的資料
3 - 不允許接收和發送更多的資料(和close()一樣)
一切就是這麼簡單:)
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
你是誰: getpeerbyname()
可能你還想知道是誰正在和你串連,那麼看下面:
- ------
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
- ------
參數addr是一個指向'struct sockaddr'或者'struct sockaddr_in'的指標。
如果執行成功,我們就得到了對方的地址了,然後用inet_ntoa()用gethostbyad
dr()來得到對方更多的資訊。如果還想知道更多的,請參閱RFC 1413。
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
我是誰: gethostname()
- ------
#include <unistd.h>
int gethostname(char *hostname, size_t size);
- ------
hostname是一個存放主機名稱字的字元數組
返回的hostname可以作為gethostbyname()的參數,這樣又可以得到自己的IP地址了。
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
我的IP是多少?
現在把我們所學的集中起來,逐步就可以完成一個實用的程式了。還是來研究一下如下的情況:
$ telnet microsoft.com
Trying 206.163.24.176 (not the real address but I'm too lazy to try
)
我們看到,telnet程式所做的第一件事情是網域名稱解析!這個工作是由gethostbyname()完成的。
- ------
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
- ------
一個特殊的結構hostent:
- ------
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
#define h_addr h_addr_list[0]
- ------
這個結構可以被分為:
h_name - 正式的機器名
h_aliases - 一個以NULL結尾的字串,表示機器的別名。
h_addrtype - 地址所使用的類型,通常是AF_INET。
h_length - 用位元組數表示的地址長度。
h_addr_list - 一個以0結尾的數組,表示機器的網路地址,以網路的位元組順序排列。
h_addr - 在h_addr_list裡的第一個地址。
gethostbyname()返回一個指向hostent結構的指標或者是一個NULL指標表示錯誤(儘管如此,錯誤碼沒有設定!)。
現在我們就來完成我們的DNS程式:
- ------
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
struct hostent *h;
if (argc != 2) { /* error checking on the command line */
fprintf(stderr,"Usage: getip <host name>\n");
exit(1);
}
if ((h=gethostbyname(argv[1])) == NULL) { /* get the host info */
herror("gethostbyname");
exit(1);
}
printf("Host name : %s\n", h->h_name);
printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr)));
return 0;
}
- ------
用gcc -o getip getip.c就可以了完成了。
=+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==+==
+==+==+=
客戶機和伺服器程式
好了,讓我們用一個小的客戶機-伺服器應用來結束本文的討論。
這個小程式的目的就是讓一個使用者和伺服器串連,接收預先定義好的資料,然後斷開。不過這個程式中有很多錯誤需要你來修改,作為你的作業來完成:)。
- ------
<++> socket/server.c
/* SERVER PROGRAM */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define PORT 1500 /* the port users will be connecting to */
#define BACKLOG 5 /* how many pending connections queue will hold */
main()
{
int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */
struct sockaddr_in my_addr; /* our address information */
struct sockaddr_in their_addr; /* their address information */
int sin_size;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* remember to error check (-1 on error) */
my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(PORT); /* short, network byte order */
my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
listen(sockfd, BACKLOG)
while(1) { /* start out accept() loop */
sin_size = sizeof(struct sockaddr_in);
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)
printf("server: got connection from %s\n", inet_ntoa(their_addr.sin_ad
dr));
fork(); /* this is the child process */
send(new_fd, "Hello, world!\n", 14, 0)
close(new_fd);
exit(0);
while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */
}
}
/* END SERVER PROGRAM, REMEMBER TO DO YOUR ERROR CHECKING */
<-->
<++> socket/client.c
/* CLIENT PROGRAM */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT 1500 /* the port client will be connecting to */
#define MAXDATASIZE 100 /* max number of bytes we can get at once */
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in their_addr; /* connector's address information */
if (argc != 2) {
fprintf(stderr,"Usage: client <host name>\n");
exit(1);
}
he = gethostbyname(argv[1]); /* get the host info */
/* did you check for errors? */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
their_addr.sin_family = AF_INET; /* host byte order */
their_addr.sin_port = htons(PORT); /* short, network byte order */
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero), 8); /* zero the rest of the struct */
connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr
));
numbytes = recv(sockfd, buf, MAXDATASIZE, 0);
buf[numbytes] = '\0';
printf("Received: %s",buf);
close(sockfd);
return 0;
}
/* END CLIENT...YOU CHECKED FOR ERRORS RIGHT? */
<-->