1、socket
每一條TCP連接兩個端點,TCP連接的端點交錯socket
socket=(IP地址:端口號)———>標示了網絡上唯一的一個進程
每一條TCP連接被兩個socket(即socket pair)唯一的確定。
TCP連接={socket1,socket2}={{IP1,port1},{IP2,port2}}
2、網絡字節序
1)大端:大端放在該值的起始地址;一個數據的高字節放在起始地址
2)小端:小端放在該值的起始地址;一個數據的低字節放在起始地址
3)數組:數組元素是按照元素個數開闢完空間,將第一個元素放在低地址,而第一個元素如果是大端,就將其高字節放在元素的起始地址處
4)網絡字節序
網絡字節序是大端的,讀的時候認爲讀到的第一個字節是高字節;所以小端要轉換成大端
發數據:從低地址——>高地址 ;收數據:從低地址——>高地址
主機字節序——網絡字節序 網絡字節序——主機字節序
3、socket地址的數據類型
各種socket地址結構體的開頭都是相同的,前16位表示整個結構體的長度(並不是所有UNIX的實現 都有長度字段,如Linux就沒有),後16位表示地址類型。
4、TCP
幾個需要注意的點:
1)socket中的數據格式是:SOCK_STREAM
2) bind之前需要將容器struct sockaddr_in 進行初始化:
端口號:將主機序列轉成網絡序列;
IP地址:字符串轉成整形
3)int listen(sockfd,backlog) 需要了解backlog是什麼意思
listen狀態下可以監聽多個client,所以可以backlog個client處於連接等待狀態,超過
backlog個的就忽略
4)accept()用於被動等待連接,其中有兩個參數需要注意
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
addr:是一個傳出參數,該函數返回時,將client的端口號和IP地址填充進去如果
addr=NULL,表示不關心addr的地址
addrlen:是一個傳入傳出參數,進去的時候是定義的addr結構體的大小,傳出的是實
際client的addr 的大小(有可能實際的小於定義的)
5)client端不需要bind函數去綁定,也不建議bind,但是常規上是可以bind的
6)connect連接的時候填充的是server的地址
代碼1:單server——單client
1)server.c
思考:在客戶端的read函數要有正確的處理
如果read的返回值:大於0,說明讀到數據了,就打印在屏幕上;
等於0,說明服務器沒有再給客戶端發信息,要關閉客戶端
(下面說明這種情況,服務器先主動關閉後,向客戶端發送FIN,客戶端發送ACK相應,然後客戶端繼續發信息,服務器會以RST響應,這個時候如果客戶端繼續調用read,由於客戶端已經收到了服務器的FIN,所以read會返回0)
如果客戶端對於read的返回值沒有進行處理,繼續發信息給服務器,這個時候如果客戶端在已經收到RST的情況下,想要調用write函數,內核會發送一個SIGPIPE信號,來終止進程,所以這個信號也需要捕獲處理
如果在read之前,服務器就崩潰了,read的會阻塞等待,那麼它會等很長時間纔會放棄連接
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h> const short _PORT=8080; #define _IP "127.0.0.1" int main() { //1.create socket int listenfd=socket(AF_INET,SOCK_STREAM,0); if(listenfd<0) { perror("socket"); return 1; } //2.1initial sockaddr_in struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(_PORT); local.sin_addr.s_addr=inet_addr(_IP); if(bind(listenfd,(struct sockaddr*)&local,sizeof(local))<0) { perror("bind"); return 2; } int ret=listen(listenfd,5); if(ret<0) { perror("listen"); return 3; } while(1) { struct sockaddr_in peer; socklen_t len=sizeof(peer); int fd=accept(listenfd,(struct sockaddr *)&peer,&len); if(fd<0) { perror("accept"); return 4; } else{ char buf[1024]; while(1) { memset(buf,'\0',1024); ssize_t _s=read(fd,buf,sizeof(buf)-1); if(_s>0) { //printf("client# %s",buf); buf[_s]='\0'; write(fd,buf,sizeof(buf)-1); printf("client# %s",buf); // write(fd,buf,sizeof(buf)-1); } else if(_s==0) { printf("read done ...\n"); break; } else{ break; } } close(fd); break; } } return 0; }
2)client.c
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> const short _PORT=8080; const char *_IP="127.0.0.1"; int main() { //1creat socket int fd=socket(AF_INET,SOCK_STREAM,0); if(fd<0) { perror("socket"); return 1; } struct sockaddr_in remote; remote.sin_family=AF_INET; remote.sin_port=htons(_PORT); remote.sin_addr.s_addr=inet_addr(_IP); int ret=connect(fd,(struct sockaddr *)&remote,sizeof(remote)); if(ret<0) { perror("connect"); return 2; } char buf[1024]; while(1) { memset(buf,'\0',sizeof(buf)); printf("Please Enter:\n"); ssize_t _s=read(0,buf,sizeof(buf)-1); if(_s>0) { buf[_s]='\0'; write(fd,buf,strlen(buf)); memset(buf,'\0',sizeof(buf)); ssize_t _ss=read(fd,buf,sizeof(buf)-1); if(_ss>0) { buf[_ss]='\0'; printf("%s",buf); } else { break; } } } return 0; }
代碼2:用fork子進程實現 單server——多client (client代碼同上,需要修改的只有server端)
總結:
(1)子進程可以關閉不必要的文件描述符或者釋放其他資源,因爲使用fork後,如果子進程不調用exec以使用新的進程空間的話,子進程會複製父進程的進程空間內容,包括數據段等(代碼段是共享的,數據段等採用一種寫時複製的策略來提高性能)。所以不必要的資源可以儘快釋放。
(2)由於子進程在退出後會成爲殭屍進程,這些信息會遺留在父進程內,所以父進程註冊所監聽的信號及其處理函數(SIGCHLD是子進程退出時向父進程發送的信號,默認情況下是不採取任何動作,所以我們需要調用wait或者waitpid來徹底清除子進程。)較爲常用的信號註冊和信號處理可以在fork前調用,使得子進程也可以完成同樣的功能。
(3)最後父進程要關閉已經accept返回的socket文件描述符,因爲父進程不處理相應活動,而是交由fork出來的子進程來處理。
(4)使用fork的時候,要注意它對父子進程有執行順序是不做任何限制的(vfork的話會先運行子進程),所以必要的時候可以讓進程睡眠或者用消息喚醒。
使用fork會有以下問題:
1 fork是昂貴的,內存映像要從父進程拷貝到子進程,所有描述字要在子進程中複製等等,還有進程的上下文切換開銷。
2 fork子進程後,需要用 進程間通信IPC 在父子進程之間傳遞信息,特別是fork內的信息。
信號處理函數實現如下:
void sig_child(int signo) //父進程對子進程結束的信號處理
{
pid_t pid;
int stat;
while( (pid=waitpid(-1,&stat,WNOHANG))>0)
printf("child %d terminated\n",pid);
return;
}
這樣一來我們就可以應付子進程退出後的殘留信息問題。注意這裏使用waitpid,其中使用WNOHANG參數來防止父進程阻塞在wait上。
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h> #include <stdlib.h> const short _PORT=8080; #define _IP "127.0.0.1" int main() { int listenfd=socket(AF_INET,SOCK_STREAM,0); if(listenfd<0) { perror("socket"); return 1; } struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(_PORT); local.sin_addr.s_addr=inet_addr(_IP); if(bind(listenfd,(struct sockaddr *)&local,sizeof(local))<0) { perror("bind"); return 2; } if(listen(listenfd,5)<0) { perror("listen"); return 3; } while(1) { struct sockaddr_in peer; socklen_t len=sizeof(peer); int fd=accept(listenfd,(struct sockaddr*)&peer,&len); int id=fork(); if(id==0) { close(listenfd); while(1) { char buf[1024]; memset(buf,'\0',sizeof(buf)); ssize_t _s=read(fd,buf,sizeof(buf)-1); if(_s>0) { buf[_s]='\0'; printf("client#%s\n",buf); write(fd,buf,sizeof(buf)-1); } } close(fd); exit(0); }else{ close(fd); } } return 0; }
代碼3:用線程實現 單server——多client(client代碼同上)
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> const short _PORT=8080; #define _IP "127.0.0.1" void *run(void *arg) { char buf[1024]; int fd=(int)arg; while(1) { memset(buf,'\0',sizeof(buf)); ssize_t _s=read(fd,buf,sizeof(buf)-1); if(_s>0) { buf[_s]='\0'; write(fd,buf,strlen(buf)); printf("client# %s",buf); } else if(_s==0) { printf("read done...\n"); close(fd); break; } else break; } exit(0); } int main() { int listenfd=socket(AF_INET,SOCK_STREAM,0); if(listenfd<0) { perror("socket"); return 1; } struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(_PORT); local.sin_addr.s_addr=inet_addr(_IP); if(bind(listenfd,(struct sockaddr *)&local,sizeof(local))<0) { perror("bind"); return 2; } if(listen(listenfd,5)<0) { perror("listen"); return 3; } while(1) { struct sockaddr_in peer; socklen_t len=sizeof(peer); int fd=accept(listenfd,(struct sockaddr *)&peer,&len); if(fd>0) { pthread_t id; // int ret=pthread_create(&id,NULL,run,(void *)fd); int ret=pthread_create(&id,NULL,run,(void*)fd); if(ret<0) { perror("pthread_create"); } //pthread_detach(id); } } close(listenfd); return 0; }
4、關於bind:Address Already in use的解決方案及分析
當./server跑起來./client跑起來
情況是這個的用netstat -nltp查看tcp服務
這個服務就是server端的服務
這個時候我們在./server那個終端窗口下輸入 ctl+c
這個時候./server終止屬於四次揮手中主動關閉的那一個人,意味着他在發送最後一個ACK後還要經歷一個time_wait的狀態等待2MSL。
1)如果在關閉之後在./server關閉./client之後立即運行就會發現這個一個現象-------TIME_WAIT
這裏的原因就是time_wait:需要等待2msl的時間
2)如果只關閉了./server之後在輸入./server (./client沒有被終止)------FIN_WAIT2
這裏的原因是隻完成了半關閉,./server一直在等./client關閉
他說綁定的端口已經被佔用了,那是哪個端口不能用呢,就是第一次socket綁定的8080不能用;
其實我們想做到的是關閉accept這個連接,對於listen的socket並不想關閉;也就是說我們只不過是不想連接了但是我們還是需要繼續監聽的,這兩個行爲佔用的端口號相同,但是IP地址不同,建立連接的時候需要具體的IP地址,而監聽的時候用的IP地址是網絡地址。
這裏用一個函數可以做到 setsockopt()設置socket描述符的 選項SO_REUSEADDR爲1,表 允許創建端 號相同但IP地址不同的多個socket描述符。