使用TCP協議的socket
1.網絡字節序
由於在主機存儲為小端序,網絡傳輸為大端序,並且在網絡中需要讀取IP號和端口號,所以發送端要將小端序轉為大端序,接收端將大端序轉為小端序
#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);
表示host,n表示network,l表示32位長整數,s表示16位短整數。
2.IP地址轉換函數
#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);
3.構造一個sockaddr
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本地任意IP(由於網卡可能綁定了多個IP)
servaddr.sin_port = htons(8000);
這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址,端口號為8000。
4.socket函數
int socket(int domain, int type, int protocol);
參數: domain:1. AF_INET (ipv4) 2.AF_INET6 (ipv6)
type: 1. SOCK_STREAM (數據流) 2.SOCK_DGRAM(數據報) 3.SOCK_RAW(ICMP使用)
protocol: 0 默認協議
返回值:成功返回一個新的文件描述符,失敗返回-1,設置errno
5.綁定函數:bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
將構造的sockaddr和創建的socket綁定在一起。
參數:
1.sockfd:socket文件描述符
2.addr:構造出IP地址加端口號
3.addrlen:sizeof(addr)長度
返回值:成功返回0,失敗返回-1, 設置errno
服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號后就可以向服務器發起連接,因此服務器需要調用bind綁定一個固定的網絡地址和端口號。
6.監聽函數:listen
int listen(int sockfd, int backlog);
參數:1.sockfd:socket文件描述符
2.backlog:排隊建立3次握手隊列和剛剛建立3次握手隊列的鏈接數和
返回值:listen()成功返回0,失敗返回-1。
7.accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
參數: 1.sockdf:socket文件描述符
2.addr:傳出參數,返回鏈接客戶端地址信息,含IP地址和端口號
3.addrlen:傳入傳出參數(值-結果),傳入sizeof(addr)大小,函數返回時返回真正接收到地址結構體的大小
返回值:成功返回一個新的socket文件描述符,用於和客戶端通信,失敗返回-1,設置errno
三方握手完成后,服務器調用accept()接受連接
accept()的參數listenfd是先前的監聽文件描述符,而accept()的返回值是另外一個文件描述符connfd,之后與客戶端之間就通過這個connfd通訊,最后關閉connfd斷開連接,而不關閉listenfd,再次回到循環開頭listenfd仍然用作accept的參數。accept()成功返回一個文件描述符,出錯返回-1。
8.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
客戶端需要調用connect()連接服務器,connect和bind的參數形式一致,區別在於bind的參數是自己的地址,而connect的參數是對方的地址。connect()成功返回0,出錯返回-1。
C/S模型-TCP
服務器調用socket()、bind()、listen()完成初始化后,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化后,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到后從connect()返回,同時應答一個ACK段,服務器收到后從accept()返回。
數據傳輸的過程:建立連接后,TCP協議提供全雙工的通信服務,但是一般的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。因此,服務器從accept()返回后立刻調用read(),讀socket就像讀管道一樣,如果沒有數據到達就阻塞等待,這時客11.3節C/S模型-TCP 123
戶端調用write()發送請求給服務器,服務器收到后從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到后從read()返回,發送下一
條請求,如此循環下去。如果客戶端沒有更多的請求了,就調用close()關閉連接,就像寫端關閉的管道一樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了連接,也調用close()關閉連接。
注意,任何一方調用close()后,連接的兩個傳輸方向都關閉,不能再發送數據了。如果一方調用shutdown()則連接處於半關閉狀態,仍可接收對方發來的數據。
可以看出在TCP建立連接后發送數據:可以用send和recv,也可以用read和write。
實例:server.c的作用是從客戶端讀字符,然后將每個字符轉換為大寫並回送給客戶端。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MAXLINE 80 #define SERV_PORT 8000 int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; //創建socket listenfd = socket(AF_INET, SOCK_STREAM, 0); //設置sockaddr bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); //將socket和sockaddr綁定 bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //監聽 listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr);//每次都要更新 connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); n = read(connfd, buf, MAXLINE); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); write(connfd, buf, n); close(connfd); } }
client.c的作用是從命令行參數中獲得一個字符串發給服務器,然后接收服務器返回的

/* client.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; char *str; if (argc != 2) { fputs("usage: ./client message\n", stderr); exit(1); } str = argv[1]; //建立socket sockfd = socket(AF_INET, SOCK_STREAM, 0); //設置sockaddr bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);//將十進制ip地址轉化 servaddr.sin_port = htons(SERV_PORT); //發起連接 connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //向服務器發送數據 write(sockfd, str, strlen(str)); //收數據 n = read(sockfd, buf, MAXLINE); printf("Response from server:\n"); //打印到輸出設備 write(STDOUT_FILENO, buf, n); //關閉套接字 close(sockfd); return 0; }
注意:recv和send函數 與 read和write函數類似,只多了一個參數flag 一般默認為0.
由於客戶端不需要固定的端口號,因此不必調用bind(),客戶端的端口號由內核自動分配。注意,客戶端不是不允許調用bind(),只是沒有必要調用bind()固定一個端口號,服務器也不是必須調用bind(),但如果服務器不調用bind(),內核會自動給服務器分配監聽端口,每次啟動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。
C/S模型-UDP
服務器端:

/* server.c */ #include <stdio.h> #include <string.h> #include <netinet/in.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int sockfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; //創建socket sockfd = Socket(AF_INET, SOCK_DGRAM, 0); //設置sockaddr bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); //將socket與sockaddr綁定 Bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); //接收來自客戶端的數據 n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len); if (n == -1) perr_exit("recvfrom error"); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); //向服務器端發送數據 n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); if (n == -1) perr_exit("sendto error"); } }
客戶端:

/* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; int sockfd, n; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; socklen_t servaddr_len; //創建socket sockfd = Socket(AF_INET, SOCK_DGRAM, 0); //設置sockaddr bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); while (fgets(buf, MAXLINE, stdin) != NULL) { //向服務器端發送數據 n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); if (n == -1) perr_exit("sendto error"); //從客戶端接收數據 n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0); if (n == -1) perr_exit("recvfrom error"); //顯示到屏幕上 Write(STDOUT_FILENO, buf, n); } //關閉套接字 Close(sockfd); return 0; }
recvfrom和sendto 的第五個參數為傳出參數,記錄發來的數據源sockaddr,服務器根據這個參數記錄客戶端的socket信息,並發送數據。