socket這個詞可以表示很多概念:
在TCP/IP協議中,“IP地址+TCP或UDP端口號”唯一標識網絡通訊中的一個進程,“IP地址+端口號”就稱為socket。
在TCP協議中,建立連接的兩個進程各自有一個socket來標識,那么這兩個socket組成的socket pair就唯一標識一個連接。
socket本身有“插座”的意思,因此用來描述網絡連接的一對一關系。
TCP/IP協議最早在BSD UNIX上實現,為TCP/IP協議設計的應用層編程接口稱為socket API。
預備知識
網絡字節序
我們已經知道,內存中的多字節數據相對於內存地址有大端和小端之分,磁盤文件中的多字節數據相對於文件中的偏移地址也有大端小端之分。
網絡數據流同樣有大端小端之分,那么如何定義網絡數據流的地址呢?發送主機通常將發送緩沖區中的數據按內存地址從低到高的順序發出,
接收主機把從網絡上接到的字節依次保存在接收緩沖區中,也是按內存地址從低到高的順序保存,因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,后發出的數據是高地址。
TCP/IP協議規定,網絡數據流應采用大端字節序,即低地址高字節。例如UDP段格式,地址0-1是16位的源端口號,如果這個端口號是1000(0x3e8),則地址0是0x03,
地址1是0xe8,也就是先發0x03,再發0xe8,這16位在發送主機的緩沖區中也應該是低地址存0x03,高地址存0xe8。但是,如果發送主機是小端字節序的,這16位被解釋成0xe803,而
不是1000。因此,發送主機把1000填到發送緩沖區之前需要做字節序的轉換。同樣地,接收主機如果是小端字節序的,接到16位的源端口號也要做字節序的轉換。如果主機是大端字節
序的,發送和接收都不需要做轉換。同理,32位的IP地址也要考慮網絡字節序和主機字節序的問題。
為使網絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調用以下庫函數做網絡字節序和主機字節序的轉換。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h表示host,n表示network,l表示32位長整數,s表示16位短整數。
如果主機是小端字節序,這些函數將參數做相應的大小端轉換然后返回,如果主機是大端字節序,這些函數不做轉 換,將參數原封不動地返回。
IP地址轉換函數 早期 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp); in_addr_t inet_addr(const char *cp); char *inet_ntoa(struct in_addr in); 只能處理IPv4的ip地址 不可重入函數 注意參數是struct in_addr 現在 #include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst); const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 支持IPv4和IPv6
其中inet_pton和inet_ntop不僅可以轉換IPv4的in_addr,還可以轉換IPv6的in6_addr,因此函數接口是void *addrptr
sockaddr數據結構
strcut sockaddr 很多網絡編程函數誕生早於IPv4協議,那時候都使用的是sockaddr結構體,為了向前兼容,現在sockaddr退化成了(void *)的作用,傳遞一個地址給函數,
至於這個函數是sockaddr_in還是sockaddr_in6,由地址族確定,然后函數內部再強制類型轉化為所需的地址類型
struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ }; struct sockaddr_in { __kernel_sa_family_t sin_family; /* Address family */ __be16 sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ /* Pad to size of `struct sockaddr'. */ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)]; }; /* Internet address. */ struct in_addr { __be32 s_addr; } struct sockaddr_in6 { unsigned short int sin6_family; /* AF_INET6 */ __be16 sin6_port; /* Transport layer port # */ __be32 sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ __u32 sin6_scope_id; /* scope id (new in RFC2553) */ }; struct in6_addr { union { __u8 u6_addr8[16]; __be16 u6_addr16[8]; __be32 u6_addr32[4]; } in6_u; #define s6_addr in6_u.u6_addr8 #define s6_addr16 in6_u.u6_addr16 #define s6_addr32 in6_u.u6_addr32 }; #define UNIX_PATH_MAX 108 struct sockaddr_un { __kernel_sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
struct sockaddr_in servaddr; /* initialize servaddr */ bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
網絡套接字函數
socket
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); domain: AF_INET 這是大多數用來產生socket的協議,使用TCP或UDP來傳輸,用IPv4的地址 AF_INET6 與上面類似,不過是來用IPv6的地址 AF_UNIX 本地協議,使用在Unix和Linux系統上,一般都是當客戶端和服務器在同一台及其上的時候使用 type: SOCK_STREAM 這個協議是按照順序的、可靠的、數據完整的基於字節流的連接。這是一個使用最多的socket類 型,這個socket是使用TCP來進行傳輸。 SOCK_DGRAM 這個協議是無連接的、固定長度的傳輸調用。該協議是不可靠的,使用UDP來進行它的連接。 SOCK_SEQPACKET 這個協議是雙線路的、可靠的連接,發送固定長度的數據包進行傳輸。必須把這個包完整的 接受才能進行讀取。 SOCK_RAW 這個socket類型提供單一的網絡訪問,這個socket類型使用ICMP公共協議。(ping、traceroute使 用該協議) SOCK_RDM 這個類型是很少使用的,在大部分的操作系統上沒有實現,它是提供給數據鏈路層使用,不保證數 據包的順序 protocol: 0 默認協議 返回值: 成功返回一個新的文件描述符,失敗返回-1,設置errno
socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描
述符,應用程序可以像讀寫文件一樣用read/write在網絡上收發數據,如果socket()調
用出錯則返回-1。對於IPv4,domain參數指定為AF_INET。對於TCP協議,type參數指定為
SOCK_STREAM,表示面向流的傳輸協議。如果是UDP協議,則type參數指定為SOCK_DGRAM,
表示面向數據報的傳輸協議。protocol參數的介紹從略,指定為0即可。
bind
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); sockfd: socket文件描述符 addr: 構造出IP地址加端口號 addrlen: sizeof(addr)長度 返回值: 成功返回0,失敗返回-1, 設置errno
服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序
的地址和端口號后就可以向服務器發起連接,因此服務器需要調用bind綁定一個固定的網絡
地址和端口號。
bind()的作用是將參數sockfd和addr綁定在一起,使sockfd這個用於網絡通訊的文件
描述符監聽addr所描述的地址和端口號。前面講過,struct sockaddr *是一個通用指針類
型,addr參數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需
要第三個參數addrlen指定結構體的長度。如:
struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(8000);
首先將整個結構體清零,然后設置地址類型為AF_INET,網絡地址為INADDR_ANY,這個
宏表示本地的任意IP地址,因為服務器可能有多個網卡,每個網卡也可能綁定多個IP地址,
這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用哪
個IP地址,端口號為8000。
listen
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); sockfd: socket文件描述符 backlog: 排隊建立3次握手隊列和剛剛建立3次握手隊列的鏈接數和
查看系統默認backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
典型的服務器程序可以同時服務於多個客戶端,當有客戶端發起連接時,服務器調用的
accept()返回並接受這個連接,如果有大量的客戶端發起連接而服務器來不及處理,尚未
accept的客戶端就處於連接等待狀態,listen()聲明sockfd處於監聽狀態,並且最多允許有
backlog個客戶端處於連接待狀態,如果接收到更多的連接請求就忽略。listen()成功返回
0,失敗返回-1。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); sockdf: socket文件描述符 addr: 傳出參數,返回鏈接客戶端地址信息,含IP地址和端口號 addrlen: 傳入傳出參數(值-結果),傳入sizeof(addr)大小,函數返回時返回真正接收到地址結 返回值: 成功返回一個新的socket文件描述符,用於和客戶端通信,失敗返回-1,設置errno
三方握手完成后,服務器調用accept()接受連接,如果服務器調用accept()時還沒有
客戶端的連接請求,就阻塞等待直到有客戶端連接上來。addr是一個傳出參數,accept()
返回時傳出客戶端的地址和端口號。addrlen參數是一個傳入傳出參數(value-result
argument),傳入的是調用者提供的緩沖區addr的長度以避免緩沖區溢出問題,傳出的是客
戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區)。如果給addr參數傳
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
connect
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); sockdf: socket文件描述符 addr: 傳入參數,指定服務器端地址信息,含IP地址和端口號 addrlen: 傳入參數,傳入sizeof(addr)大小 返回值: 成功返回0,失敗返回-1,設置errno
server.c
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <strings.h> #include <arpa/inet.h> #include <unistd.h> #include <ctype.h> #define SERV_PORT 8000 int main(void) { int sfd, cfd; int i, len; struct sockaddr_in serv_addr, client_addr; char buf[4096], client_ip[128]; socklen_t addr_len; //AF_INET:ipv4 SOCK_STREAM:流協議 0:默認協議(tcp,udp) sfd = socket(AF_INET, SOCK_STREAM, 0); //綁定前先構造出服務器地址 bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; //網絡字節序 serv_addr.sin_port = htons(SERV_PORT); //INADDR_ANY主機所有ip serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); //服務器能接收並發鏈接的能力 listen(sfd, 128); printf("wait for connect ...\n"); addr_len = sizeof(client_addr); //阻塞,等待客戶端鏈接,成功則返回新的文件描述符,用於和客戶端通信 cfd = accept(sfd, (struct sockaddr *)&client_addr, &addr_len); printf("client IP:%s\t%d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)), ntohs(client_addr.sin_port)); while (1) { //阻塞接收客戶端數據 len = read(cfd, buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); //處理業務 for (i = 0; i < len; i++) buf[i] = toupper(buf[i]); //返回給客戶端結果 write(cfd, buf, len); } close(cfd); close(sfd); return 0; }
client
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <arpa/inet.h> #define SERV_PORT 8000 int main(int argc, char *argv[]) { int sfd, len; struct sockaddr_in serv_addr; char buf[4096]; if (argc < 2) { printf("./client serv_ip\n"); return 1; } sfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1], &serv_addr.sin_addr.s_addr); connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); while (fgets(buf, sizeof(buf), stdin)) { write(sfd, buf, strlen(buf)); len = read(sfd, buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); } return 0; }
多進程並發服務器

#include <stdlib.h> #include <errno.h> #include <sys/socket.h> void perr_exit(const char *s) { perror(s); exit(1); } int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) { int n; again: if ( (n = accept(fd, sa, salenptr)) < 0) { if ((errno == ECONNABORTED) || (errno == EINTR)) goto again; else perr_exit("accept error"); } return n; } void Bind(int fd, const struct sockaddr *sa, socklen_t salen) { if (bind(fd, sa, salen) < 0) perr_exit("bind error"); } void Connect(int fd, const struct sockaddr *sa, socklen_t salen) { if (connect(fd, sa, salen) < 0) perr_exit("connect error"); } void Listen(int fd, int backlog) { if (listen(fd, backlog) < 0) perr_exit("listen error"); } int Socket(int family, int type, int protocol) { int n; if ( (n = socket(family, type, protocol)) < 0) perr_exit("socket error"); return n; } ssize_t Read(int fd, void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = read(fd, ptr, nbytes)) == -1) { if (errno == EINTR) goto again; else return -1; } return n; } ssize_t Write(int fd, const void *ptr, size_t nbytes) { ssize_t n; again: if ( (n = write(fd, ptr, nbytes)) == -1) { if (errno == EINTR) goto again; else return -1; } return n; } void Close(int fd) { if (close(fd) == -1) perr_exit("close error"); } ssize_t Readn(int fd, void *vptr, size_t n) { size_t nleft; ssize_t nread; char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nread = read(fd, ptr, nleft)) < 0) { if (errno == EINTR) nread = 0; else return -1; } else if (nread == 0) break; nleft -= nread; ptr += nread; } return n - nleft; } ssize_t Writen(int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; ptr += nwritten; } return n; } static ssize_t my_read(int fd, char *ptr) { static int read_cnt; static char *read_ptr; static char read_buf[100]; if (read_cnt <= 0) { again: if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) { if (errno == EINTR) goto again; return -1; } else if (read_cnt == 0) return 0; read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1; } ssize_t Readline(int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen; n++) { if ( (rc = my_read(fd, &c)) == 1) { *ptr++ = c; if (c == '\n') break; } else if (rc == 0) { *ptr = 0; return n - 1; } else return -1; } *ptr = 0; return n; }

#ifndef __WRAP_H_ #define __WRAP_H_ void perr_exit(const char *s); int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr); void Bind(int fd, const struct sockaddr *sa, socklen_t salen); void Connect(int fd, const struct sockaddr *sa, socklen_t salen); void Listen(int fd, int backlog); int Socket(int family, int type, int protocol); ssize_t Read(int fd, void *ptr, size_t nbytes); ssize_t Write(int fd, const void *ptr, size_t nbytes); void Close(int fd); ssize_t Readn(int fd, void *vptr, size_t n); ssize_t Writen(int fd, const void *vptr, size_t n); static ssize_t my_read(int fd, char *ptr); ssize_t Readline(int fd, void *vptr, size_t maxlen); #endif
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <strings.h> #include <stdlib.h> #include <ctype.h> #include <signal.h> #include "wrap.h" #define SERV_PORT 8000 void do_sig(int num) { while (waitpid(0, NULL, WNOHANG) > 0) ; } int main(int argc, char *argv[]) { int lfd, cfd, len, i; int serv_port = SERV_PORT; char buf[1024], client_ip[128]; struct sockaddr_in serv_addr, client_addr; socklen_t client_len; pid_t pid; struct sigaction act; act.sa_handler = do_sig; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaction(SIGCHLD, &act, NULL); if (argc == 2) serv_port = atoi(argv[1]); lfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons((short)serv_port); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); Listen(lfd, 128); printf("wait for connect...\n"); while (1) { client_len = sizeof(client_addr); cfd = Accept(lfd, (struct sockaddr *)&client_addr, &client_len); printf("client:%s\t%d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)), ntohs(client_addr.sin_port)); pid = fork(); if (pid == 0) { //in child Close(lfd); while (1) { len = Read(cfd, buf, sizeof(buf)); if (len <= 0) break; Write(STDOUT_FILENO, buf, len); for (i = 0; i < len; ++i) buf[i] = toupper(buf[i]); Write(cfd, buf, len); } Close(cfd); return 0; } else if (pid > 0) { //in parent Close(cfd); } else { perror("fork"); exit(1); } } Close(lfd); return 0; }
客戶端
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <arpa/inet.h> #define SERV_PORT 8000 int main(int argc, char *argv[]) { int sfd, len; struct sockaddr_in serv_addr; char buf[4096]; if (argc < 2) { printf("./client serv_ip\n"); return 1; } sfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1], &serv_addr.sin_addr.s_addr); connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); while (fgets(buf, sizeof(buf), stdin)) { write(sfd, buf, strlen(buf)); len = read(sfd, buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); } return 0; }
多線程並發服務器
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <strings.h> #include <stdlib.h> #include <ctype.h> #include <signal.h> #include "wrap.h" #define SERV_PORT 8000 void *do_work(void *arg) { char buf[1024]; int len, i; int cfd = (int)arg; pthread_detach(pthread_self()); while (1) { len = Read(cfd, buf, sizeof(buf)); if (len <= 0) break; Write(STDOUT_FILENO, buf, len); for (i = 0; i < len; ++i) buf[i] = toupper(buf[i]); Write(cfd, buf, len); } Close(cfd); return 0; } int main(int argc, char *argv[]) { int lfd, cfd; int serv_port = SERV_PORT; char client_ip[128]; struct sockaddr_in serv_addr, client_addr; socklen_t client_len; pthread_t tid; if (argc == 2) serv_port = atoi(argv[1]); lfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons((short)serv_port); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); Listen(lfd, 128); printf("wait for connect...\n"); while (1) { client_len = sizeof(client_addr); cfd = Accept(lfd, (struct sockaddr *)&client_addr, &client_len); printf("client:%s\t%d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)), ntohs(client_addr.sin_port)); pthread_create(&tid, NULL, do_work, (void *)cfd); } return 0; }
多路I/O復用
select服務器
poll服務器
epoll服務器