一、經常使用函數介紹git
int socket(int domain,int type,int protocol); /* domain:AF_INET設爲IPV4 type:SOCK_STREAM對應TCP,SOCK_DGRAM對應UDP protocol:設0 返回值:返回一個套接字,失敗返回-1 */
int bind(int sockfd,struct sockaddr *my_addr,int addrlen); /* sockfd:由socket()調用返回的須要綁定的套接字 my_addr:sockaddr類型的地址 addrlen:sizeof(sockaddr)。 返回值:成功返回0;失敗返回-1 */
struct sockaddr_in { short sin_family; /* 地址類型,TCPIP協議只能填AF_INET */ unsigned short sin_port; /*使用端口號 */ struct in_addr sin_addr; /* 網絡地址,如需綁定全部地址,填INADDR_ANY */ unsigned char sin_zero[8]; /* 填0便可 */ };//通常將其強制轉換成sockaddr來使用
uint16_t htons(uint16_t hostshort); /* 把系統的16位整數調整爲「大端模式」 */
int inet_aton(const char *string, struct in_addr *addr); /* 把字符串的IP地址轉化爲in_addr結構體 */
int connect(int sockfd,struct sockaddr* serv_addr,int addrlen); /* sockfd:鏈接到的套接字 serv_addr:鏈接到的服務器地址 addrlen:sizeof(serv_addr) 返回值:失敗返回-1 */
int listen(int sockfd,int backlog);//設置服務器監聽模式 /* sockfd:須要設置監聽的服務器套接字 backlog:進入隊列中容許的鏈接的個數。 返回值:出錯返回-1 */
int accept(int sockfd,void *addr,int* addrlen);//接受已經connect並在款衝隊列中等待的套接字,隊列爲空時默認進入阻塞狀態,直到有客戶端進行connect() /* sockfd:正在監聽端口的套接字 addr:用於存儲客戶的地址結構體 addrlen:sizeof(struct sockaddr_in) 返回值:失敗-1 */
int send(int sockfd,const void* msg,int len,int flags);//TCP發送數據 /* sockfd:發送目標的套接字 msg:須要發送數據的頭指針 len:數據的字節長度 flags:設爲0 返回值:返回實際發送的字節數。注意,返回值可能比須要發送的字節數要少,此時須要再次發送剩下的字節。如失敗返回-1 */
int recv(int sockfd,void* buf,int len,unsigned int flags);//接受TCP數據 /* sockfd:是要讀取的套接口字 buf:保存數據的內存入口。 len:緩衝區的最大長度。注意,緩衝區不需用完 flags:設爲0 返回值:返回實際讀取到緩衝區的字節數,若是出錯則返回-1。 */
二、服務器端流程github
設置服務器地址安全
//設置一個socket地址結構server_addr,表明服務器internet地址, 端口 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); //把一段內存區的內容所有設置爲0 server_addr.sin_family = AF_INET;// server_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY是全0特殊地址,用於含有多IP地址的服務器,表示同時綁定本身的全部地址 server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);//小端數轉大端數函數
建立服務器套接字服務器
//建立用於internet的流協議(TCP)socket,用server_socket表明服務器socket int server_socket = socket(AF_INET,SOCK_STREAM,0); if( server_socket < 0){ printf("Create Socket Failed!"); exit(1); }
綁定地址與套接字網絡
//把socket和socket地址結構聯繫起來 if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))){ printf("Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT); exit(1); }
設置服務器爲監聽狀態dom
//server_socket用於監聽 if ( listen(server_socket, LENGTH_OF_LISTEN_QUEUE) ){ printf("Server Listen Failed!"); exit(1); }
接受客戶端的鏈接socket
int new_server_socket = accept(server_socket,(struct sockaddr*)&client_addr,&length); if ( new_server_socket < 0) { printf("Server Accept Failed!\n"); break; }
從客戶端接收數據tcp
//接收客戶端發送來的信息到buffer中 length = recv(new_server_socket,buffer,BUFFER_SIZE,0);//若是對方在一次鏈接裏發送了兩次呢? if (length < 0){ printf("Server Recieve Data Failed!\n"); exit(1); } printf("\n%s",buffer);
發送數據到客戶端函數
char buffer[BUFFER_SIZE]; bzero(buffer, BUFFER_SIZE); strcpy(buffer,"Hello,World! 從服務器來!"); strcat(buffer,"\n"); //C語言字符串鏈接 //發送buffer中的字符串到new_server_socket,實際是給客戶端 send(new_server_socket,buffer,BUFFER_SIZE,0);
關閉鏈接,空出端口ui
//關閉與客戶端的鏈接 close(new_server_socket); //關閉監聽用的socket close(server_socket);
三、客戶端流程
設置客戶端地址
//設置一個socket地址結構client_addr,表明客戶機internet地址, 端口 struct sockaddr_in client_addr; bzero(&client_addr,sizeof(client_addr)); //把一段內存區的內容所有設置爲0 client_addr.sin_family = AF_INET; //internet協議族 client_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY表示自動獲取本機地址 client_addr.sin_port = htons(0); //0表示讓系統自動分配一個空閒端口
創建客戶端的套接字
//建立用於internet的流協議(TCP)socket,用client_socket表明客戶機socket int client_socket = socket(AF_INET,SOCK_STREAM,0); if( client_socket < 0) { printf("Create Socket Failed!\n"); exit(1); }
綁定客戶端地址與套接字
//把客戶機的socket和客戶機的socket地址結構聯繫起來 if( bind(client_socket,(struct sockaddr*)&client_addr,sizeof(client_addr))){ printf("Client Bind Port Failed!\n"); exit(1); }
設置服務器地址結構體
//設置一個socket地址結構server_addr,表明服務器的internet地址, 端口 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); server_addr.sin_family = AF_INET; if(inet_aton(argv[1],&server_addr.sin_addr) == 0){ //服務器的IP地址來自程序的參數,aton字符串IP地址轉化爲網絡地址格式 printf("Server IP Address Error!\n"); exit(1); } server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
創建於服務器的鏈接
socklen_t server_addr_length = sizeof(server_addr); //向服務器發起鏈接,鏈接成功後client_socket表明了客戶機和服務器的一個socket鏈接 if(connect(client_socket,(struct sockaddr*)&server_addr, server_addr_length) < 0) { printf("Can Not Connect To %s!\n",argv[1]); exit(1); }
接收、發送數據
關閉鏈接
完整代碼:https://github.com/iyjhabc/study_examples/blob/master/server.c
編譯c程序 gcc client.c -o client
運行客戶端 ./client 127.0.0.1
127.0.0.1爲保留IP地址,固定指向本地主機。
----------------------------------------------------------------------------------------------------
雖然知道各個函數怎麼用,但這幾個函數都是要組合起來使用,組合得不對使用就會出錯,下面介紹一下如何纔是正確組合TCP函數。
兩個計算機經過網絡鏈接,說白了經過兩樣東西來定位,IP和端口。套接字其實就是一個已經鏈接到某IP和某端口的一條抽象通道。知道這兩個概念,下面就容易理解了。
一、服務器
做爲服務器,通常來講不會首先鏈接別人,而是等別人主動鏈接它,他被動等待別人的鏈接。而且做爲公共使用的服務器,必須有固定的端口,不然別人怎麼知道怎麼找到你?
(1)bind().服務器的第一步是把新建的服務器socket套接字bind一個端口。此時此套接字已經跟服務器的IP和端口牢牢聯繫在一塊兒了(但還沒鏈接)。
(2)listen()與accept().此二函數是一塊兒使用的,首先把服務器的套接字設爲監聽狀態,而後在循環裏面調用accept。它被調用後會阻塞本身所在的線程,直到有客戶端connect爲止。
(3)recv().當有客戶端鏈接服務器,accept的阻塞被釋放,並返回一個已經連向該客戶端的套接字。此時服務器就能夠經過此套接字,使用recv函數接受來自客戶端的數據,並使用send給客戶端發送數據。
(4)close().accept返回的套接字使用完成後,必須使用close把它關閉。
總結來講,服務器只需本身綁定一個端口,等待客戶端的鏈接。它徹底不用管客戶端的ip和端口,由於accept返回的套接字已經包含了鏈接向客戶端IP和端口的鏈接通路。
二、客戶端
做爲客戶端,通常是主動鏈接服務器,等待服務器的迴應。客戶端必須知道想要鏈接的服務器的IP和端口。
(1)connect().首先客戶端新建一個套接字,並根據服務器的IP和端口把套接字connect到服務器,造成鏈接通路。
(2)send().鏈接成功後,就能夠利用該套接字向服務器發送數據。
(3)recvfrom().由於剛纔的套接字已經與服務器造成鏈接,所以也能夠用來接收服務器返回的數據。
這裏再說明一下,connect造成與服務器鏈接這個套接字,其實就是服務器端accept返回那個套接字,正由於如此,客戶端才能用這個套接字recv服務器返回的消息。至於爲何用recvfrom而不是recv,接下來講。
服務器與客戶端握手對話(客發-服收-服發-客收)代碼可參考如下:
客戶端:https://github.com/iyjhabc/study_examples/blob/master/tcp_data_transport_client.c
服務器:https://github.com/iyjhabc/study_examples/blob/master/tcp_data_transport.c
三、recv與recvfrom
衆所周知recv主要用在TCP中,recvfrom主要用在UDP中,但如客戶端須要等待服務器返回的消息,再做下一步運算的話,就應使用recvfrom,如上面的例子,客戶端等待服務器放置數據完成,再從新發送下一輪數據。緣由是,recv默認是非阻塞的,當客戶端發送完後調用recv,服務器還沒發送數據過來recv就已經執行完畢,所以接受不到服務器數據。recvfrom默認是阻塞的,它會等待服務器返回的數據再往下運行。
而且recv只能用於TCP,而recvfrom同時用於TCP和UDP。recvfrom參數的地址指針將會記錄接到的數據的來源地址。再有一點,recv和recvfrom參數裏面的套接字,必須是已經創建鏈接的,沒鏈接,怎麼知道接受誰呢?要監放任意的客戶端,應先使用listen和accept。
四、send
通常來講,使用一次send,便connect一次。如連續使用send,後面send的數據會丟失。爲什麼?其實聯繫服務器的原理便知道。客戶端connect後,服務器accept並返回一個鏈接雙方的套接字。此時雙方用此套接字發送接收數據。但通常來講服務器recv一次後便會close該套接字。若是此時客戶端繼續試用send,天然就發送不成功了。
總結就是,必須在套接字連同的條件下才能使用send(也是recv也是),不少狀況是你本身沒有斷開鏈接,但對方其實已經close了該套接字了,便形成了發送失敗。因此,鏈接一次發送一次數據send一次,是比較安全的作法。