進程間通信:
特點:依賴于內核,造成缺陷——無法實現多機通信。
網絡:
地址:由IP地址(IP地址是IP協議提供的一種統一的地址格式,它為互聯網上的每一個網絡和每一臺主機分配一個邏輯地址,以此來屏蔽物理地址的差異。)和端口號構成(所謂的端口,就好像是門牌號一樣,客戶端可以通過ip地址找到對應的服務器端,但是服務器端是有很多端口的,每個應用程序對應一個端口號,通過類似門牌號的端口號,客戶端才能真正的訪問到該服務器。為了對端口進行區分,將每個端口進行了編號,這就是端口號 ,端口包括邏輯端口和物理端口兩種類型),實際上是通過IP地址+端口號來區分不同服務的,端口號提供了一種訪問通道,服務器一般都是通過知名端口號來識別的。例如:對于每個TCP/IP實現來說,FTP服務器的TCP端口號就是21,每個Talnet服務器的TCP端口號都是23,每個TFTP(簡單文件傳送協議)服務器的UDP端口號都是69.
數據交流:
當涉及到數據交流的時候就會涉及到協議(說白了就是數據格式),例如:http(超文本傳輸協議是一個簡單的請求-響應協議,它通常運行在TCP之上。它指定了客戶端可能發送給服務器什么樣的消息以及得到什么樣的響應。請求和響應消息的頭以ASCII形式給出;而消息內容則具有一個類似MIME的格式。這個簡單模型是早期Web成功的有功之臣,因為它使開發和部署非常地直截了當。)、tcp(傳輸控制協議是一種面向連接的、可靠的、基于字節流的傳輸層通信協議,由IETF的RFC 793定義。)、udp(Internet 協議集支持一個無連接的傳輸協議,該協議稱為用戶數據報協議(UDP,User Datagram Protocol)。UDP 為應用程序提供了一種無需建立連接就可以發送封裝的 IP 數據包的方法。RFC 768描述了 UDP。)
socket套接字:
- 所謂套接字(Socket),就是對網絡中不同主機上的應用進程之間進行雙向通信的端點的抽象。一個套接字就是網絡上進程通信的一端,提供了應用層進程利用網絡協議交換數據的機制。從所處的地位來講,套接字上聯應用進程,下聯網絡協議棧,是應用程序通過網絡協議進行通信的接口,是應用程序與網絡協議根進行交互的接口。
- 所謂socket網絡編程,也叫作套接字網絡編程,套接字網絡編程用到的協議TCP協議和UDP協議的用的比較多。
TCP和UDP協議的區別:
- 連接方面區別:TCP提供面向連接的傳輸,通信前要先建立連接(三次握手機制)(如打電話要先撥號建立連接)。UDP是無連接的,面向報文的,即發送數據之前不需要建立連接。
- 安全方面的區別:TCP提供可靠的服務,通過TCP連接傳送的數據,無差錯,不丟失,不重復,且按序到達。UDP盡最大努力交付,即不保證可靠交付。
- 傳輸效率的區別:TCP傳輸效率相對較低。UDP傳輸效率高,適用于對高速傳輸和實時性有較高的通信或廣播通信。
- 連接對象數量的區別:TCP連接只能是點到點、一對一的。UDP支持一對一,一對多,多對一和多對多的交互通信。
- TCP首部開銷20字節,UDP的首部開銷小,只有8個字節
字節序:
字節序,即字節在電腦中存放時的序列與輸入(輸出)時的序列是先到的在前還是后到的在前。計算機硬件有兩種儲存數據的方式:大端字節序(big endian)和小端字節序(little endian)。
為什么會有小端字節序?
- 計算機電路先處理低位字節,效率比較高,因為計算都是從低位開始的。所以,計算機的內部處理都是小端字節序。
- 但是,人類還是習慣讀寫大端字節序。所以,除了計算機的內部處理,其他的場合幾乎都是大端字節序,比如網絡傳輸和文件儲存。
常見序:
- Little endian(小端字節序):將低序字節存儲在起始地址
- Big endian(大端字節序):將高序字節存儲在起始地址
- 網絡協議指定了通訊字節序—大端
- 只有在多字節數據作為整體處理時才需要考慮字節序
- 運行在同一臺計算機上的進程相互通信時,一般不用考慮字節序
- 異構計算機之間通訊,需要轉換自己的字節序為網絡字節序
什么是高低位:
- 給一個十進制整數,123456,很明顯左邊的是高位,右邊的是低位。計算機也是這樣認為的。給一個16進制數(四個二進制表示),0x12345678,以字節為單位,從高位到低位依次是 0x12、0x34、0x56、0x78。
- 例子:在內存中雙字0x01020304(DWORD)的存儲方式
地址從低到高:4000->4001->4002->4003
小端字節序: 04 03 02 01
大端字節序:01 02 03 04
socket編程步驟:
可以把服務器與客戶端之間的場景看做以下場景:一個客戶端走到五座房子(5個服務器)的面前,要訪問這五座房子中的一座房子中的一間房間。當客戶端不知道去哪一間房子時,這時候有一個人在樓上喊我是說漢語的(TCP/UDP)服務器,我的樓號是…(IP地址),我的房間號是…(端口號),然后客戶端就可以獲取服務器IP和服務器端口號進行連接。
服務器端創建步驟:
- 服務器端創建套接字(socket函數),返回網絡描述符,后續用網絡描述符進行操作
- bind()為套接字添加信息,指定服務器自己的IP地址和端口號
- 監聽網絡連接(listen()函數)
- 監聽到有客戶端接入,接收一個連接(accept()函數)
- 數據交互(利用read函數從網絡上面讀數據,利用write函數向網絡上面寫數據)
- 關閉套接字(close()函數),斷開連接
客戶端創建步驟:
- socket()創建一個通道
- connect()連接服務器,根據IP地址和端口號
- 然后進行讀寫操作
- 最后關閉套接字斷開連接
三次握手和一次揮手:
當客戶端調用connect時,觸發了連接請求,向服務器發送了SYN J包,這時connect進入阻塞狀態;服務器監聽到連接請求,即收到SYN J包,調用accept函數接收請求向客戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到服務器的SYN K ,ACK J+1之后,這時connect返回,并對SYN K進行確認;服務器收到ACK K+1時,accept返回,至此三次握手完畢,連接建立。
socket編程函數介紹
socket()函數:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
功能:這個函數建立一個協議族為domain、協議類型為type、協議編號為protocol的套接字文件描述符。參數:domain:函數socket()的參數domain用于設置網絡通信的域函數socket()根據這個參數選擇通信協議的族,通常為AF_INET,表示互聯網協議族(TCP/IP協議族)。通信協議族在文件sys/socket.h中定義。協議族決定了socket的地址類型,在通信中必須采用對應的地址domain的值及含義:AF_INET、PF_INET: IPV4因特網域AF_INET6: IPV6因特網域AF_UNIX: Unix域AF_ROUTE: 路由套接字AF_KEY: 密鑰套接字AF_UNSPEC: 未指定PF_UNIX,PF_LOCAL: 本地通信type:指定socket類型常用的socket類型有:SOCK_STREAM: Tcp連接,提供序列化的、可靠的、雙向連接的字節流,使用TCP協議。支持帶外數據傳輸SOCK_DGRAM: 數據報套接字定義了一種無連接的報,數據通過相互獨立的報文件進行傳輸,是無序的,并且不保證是可靠的、無差錯的、使用數據報協議UDP連接(無連接狀態的消息)SOCK_RAW: RAW類型,允許程序使用底層協議,原始套接字允許對底層協議如IP/ICMP進行直接訪問,功能強大但使用較為不便,主要用于協議的開發SOCK_PACKET: 這是一個專用類型,不能呢過在通用程序中使用SOCK_SEQPACKET:序列化包,提供一個序列化的、可靠的、雙向的基本連接的數據傳輸通道,數據長度定常。每次調用讀系統調用時數據需要將全部數據讀出protocol:故名思意,就是指定協議.常用的協議有:IPPROTO_TCP:TCP傳輸協議IPPTOTO_UDP:UDP傳輸協議IPPROTO_SCTP:STCP傳輸協議IPPROTO_TIPC:TIPC傳輸協議返回值:如果函數調用成功,會返回一個標識這個套接字的文件描述符,失敗的時候返回-1。
- 注意: 并不是上面的type和protocol可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應的默認協議。
- 當我們調用socket創建一個socket時,返回的socket描述字它存在于協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調用bind()函數,否則就當調用connect()、listen()時系統會自動隨機分配一個端口。
bind()函數:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:正如上面所說bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。參數:sockfd:即socket描述字,它是通過socket()函數創建了唯一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址這個地址結構根據地址創建socket時的地址協議族的不同而不同。如ipv4對應的是: struct sockaddr{unsigned short as_familf;//協議族char sa_data[14];//IP+端口號}同等替換為:使用時,強轉:(struct sockaddr *)&my_addr struct sockaddr_in {sa_family_t sin_family; /* 協議族: AF_INET */in_port_t sin_port; /* 端口號,一般用5000以上,低于3000的是操作系統的關鍵端口,這個要將它轉化為網絡字節序*/struct in_addr sin_addr; /* IP地址結構體*/unsigned char sin_zero[8]/*填充沒有實際意義只是為跟sockaddr結構體在內存中對齊,這樣兩者才能相互轉換*/};/* Internet address. */struct in_addr {uint32_t s_addr; /* address in network byte order */};ipv6對應的是: struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ };struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ };Unix域對應的是: #define UNIX_PATH_MAX 108struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };addrlen:對應的是地址的長度,結構體大小。返回值:bind()函數的返回值為0時表示綁定成功,-1表示綁定失敗
- 通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用于提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是為什么通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
- 在將一個地址綁定到socket的時候,請先將主機字節序轉換成為網絡字節序,而不要假定主機字節序跟網絡字節序一樣使用的是Big-Endian。請謹記對主機字節序不要做任何假定,務必將其轉化為網絡字節序再賦給socket。
IP地址轉換API:
舊版本:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
功能:將點分十進制IP轉化為網絡字節序存放在addr中,并返回該網絡字節序對應的整數。
參數:輸入參數cp包含ASCII表示的IP地址。輸出參數inp是將要用新的IP地址更新的結構。
返回值:如果這個函數成功,函數的返回值非零。如果輸入地址不正確則會返回零。使用這個函數并沒有錯誤碼存放在errno中,所以他的值會被忽略。in_addr_t inet_addr(const char *cp);
功能:將點分十進制IP轉化為網絡字節序(二進制位的大端存儲)。
返回值;如果失敗:返回INADDR_NONE;如果成功:返回IP對應的網絡字節序的數;char *inet_ntoa(struct in_addr in);
功能:把網絡格式的ip地址轉化為字符串形式
inet_network函數in_addr_t inet_network(const char *StrIP)
功能: 將點分十進制IP轉化為主機字節序(二進制位小端存儲)。
返回值:如果失敗:返回-1;如果成功:返回主機字節序對應的數;
舊版本的只能處理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。
listen()、connect()函數:
如果作為一個服務器,在調用socket()、bind()之后就會調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。
#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:內核為任何一個給定監聽者套接字,維護兩個隊列:1、未完成隊列,每個這樣的SYN報文段對應其中一項已由某個客戶端發出并到達服務器,而服務器正在等待相應的TCP三次握手過程,這些套接字處于SYN_REVD狀態2、已完成隊列,每個已完成TCP三次握手過程的客戶端對應其中一項,這些套接字處于ESTABLISHED狀態。參數:listen函數的第一個參數即為要監聽的socket描述字服務器的描述字第二個參數為相應socket可以排隊的最大連接個數。listen()并未開始連線,只是設置socket的listen模式。listen函數只用于服務器端。socket()函數創建的socket默認是一個主動類型的,listen函數將socket變為被動類型的,等待客戶的連接請求。connect函數的第一個參數即為客戶端的socket描述字,第二參數為服務器的socket地址,第三個參數為socket地址的長度。客戶端通過調用connect函數來建立與TCP服務器的連接。返回值:listen函數:成功返回0, 失敗返回-1.connect函數:成功返回0, 失敗返回-1.
accept()函數:
TCP服務器端依次調用socket()、bind()、listen()之后,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之后就向TCP服務器發送了一個連接請求。TCP服務器監聽到這個請求之后,就會調用accept()函數取接收請求,這樣連接就建立好了。之后就可以開始網絡I/O操作了,即類同于普通文件的讀寫I/O操作。
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
參數:accept函數的第一個參數為服務器的socket描述字,第二個參數為指向struct sockaddr *的指針,用于返回客戶端的協議地址,不關心可以設置為NULl。第三個參數為協議地址的長度。返回值:如果accpet成功,那么其返回值是由內核自動生成的一個全新的描述字,代表與返回客戶的TCP連接。失敗時返回-1
- 注意: accept的第一個參數為服務器的socket描述字,是服務器開始調用socket()函數生成的,稱為監聽socket描述字;而accept函數返回的是已連接的socket描述字。一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個已連接socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。
數據的收發:
一般用read/write函數,當然還有其他函數:
- read/write
- recv()/send()
- readv()/writev()
- recvmsg()/sendmsg()(多在UDP連接時使用)
- recvfrom()/sendto()(多在UDP連接時使用)
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recv()/send()函數:
close()函數:
在服務器與客戶端建立連接之后,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完打開的文件要調用fclose關閉打開的文件。
#include <unistd.h>
int close(int fd);close一個TCP socket的缺省行為時把該socket標記為以關閉
然后立即返回到調用進程。該描述字不能再由調用進程使用
也就是說不能再作為read或write的第一個參數。注意:close操作只是使相應socket描述字的引用計數-1只有當引用計數為0的時候,才會觸發TCP客戶端向服務器發送終止連接請求。
網絡字節序和主機字節序的轉換:
#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位短整數。
如果主機是小端字節序,這些函數將參數做相應的大小端轉換然后返回,
如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。
服務端實現可被連接功能:
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
//#include<linux/in.h> 頭文件之間可能造成沖突這個頭文件就和#include <arpa/inet.h>沖突
#include <arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include <sys/wait.h>
#include<pthread.h>
struct num
{int fd;char*write;
};
void* writeMsg(void*arg)
{int i;struct num Part;Part.write=((struct num*)arg)->write;Part.fd=((struct num*)arg)->fd;while(1){if(i==1){getchar();}i=1;memset(Part.write,'\0',128);printf("輸入要給客戶端的內容:\n");scanf("%[^\n]",Part.write);write(Part.fd,Part.write,128);}
}
int main()
{struct sockaddr_in IP;struct sockaddr_in CLI;//客戶端信息struct num CAN;int socketre;int bindre;int i=0;int newfd;pid_t fpid;pthread_t sontd;char* writebuf;char* readbuf;readbuf=(char*)malloc(128);writebuf=(char*)malloc(128);int len=sizeof(struct sockaddr_in);socketre=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(socketre==-1){printf("create fail\n");perror("socket");exit(-1);}memset(&IP,'\0',len);memset(&CLI,'\0',len);IP.sin_family=AF_INET;IP.sin_port=htons(8686);IP.sin_addr.s_addr=inet_addr("192.168.1.183");bindre=bind(socketre,(struct sockaddr*)&IP,len);if(bindre==-1){perror("bind");printf("bind fail\n");exit(-1);}listen(socketre,10);while(1){newfd=accept(socketre,(struct sockaddr*)&CLI,&len);if(newfd==-1){perror("accept");printf("accept fail\n");exit(-1);}printf("連接成功\n");printf("get client:%s\n",inet_ntoa(CLI.sin_addr));CAN.write=writebuf;CAN.fd=newfd;fpid=fork();if(fpid==0){pthread_create(&sontd,NULL,writeMsg,(void*)&CAN);while(1){memset(readbuf,'\0',128);if(read(newfd,readbuf,128)!=0){if(strcmp(readbuf,"quit")==0){write(newfd,"server is out",13);close(newfd);pthread_cancel(sontd);break;}printf("接受到消息:%s\n",readbuf);printf("輸入要給客戶端的內容:\n");}else{printf("與客戶端斷開連接\n");}}}if(fpid>0){waitpid(fpid,NULL,WNOHANG | WUNTRACED);}}return 0;
}
如果有手機的話可以下載TCP手機助手,進行連接服務器,沒有該軟件可以用以下客戶端代碼:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include<pthread.h>
struct Client
{char* write;int fd;
};
void* del(void*arg)
{struct Client CAN;CAN.write=((struct Client*)arg)->write;CAN.fd=((struct Client*)arg)->fd;while(1){memset(CAN.write,'\0',128);printf("請輸入要發送給服務端的消息:\n");scanf("%s",CAN.write);write(CAN.fd,CAN.write,strlen(CAN.write));}
}
int main()
{int socketfd;int conre;char*writebuf;char*readbuf;pthread_t th;struct Client CL;struct sockaddr_in addr;addr.sin_family=AF_INET;addr.sin_port=htons(8686);addr.sin_addr.s_addr=inet_addr("192.168.1.183");socketfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(socketfd==-1){printf("socket create fail\n");perror("socket");exit(-1);}conre=connect(socketfd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in));if(conre==-1){printf("connect fail\n");perror("connect");exit(-1);}writebuf=(char*)malloc(128);readbuf=(char*)malloc(128);CL.write=writebuf;CL.fd=socketfd;pthread_create(&th,NULL,del,(void*)&CL);while(1){memset(readbuf,'\0',128);read(socketfd,readbuf,128);printf("獲取到服務端數據:%s\n",readbuf);printf("請輸入要發送給服務端的消息:\n");}return 0;
}
補充:
- 當我們想用一個結構體或者聯合體時,可以進入/usr/include/這個文件夾下面,查找看看頭文件里面有沒有包含想要使用的結構體或者聯合體,使用以下命令:
cd /usr/include/
進入文件夾
grep "struct sockaddr_in {" * -nir
grep表示查找,雙引號內的東西是你要查找的內容的一部分
*表示在當前目錄下。-nir中 n表示顯示行號,i表示不區分大小寫,r表示逐行掃描
linux/in.h:232:struct sockaddr_in {
得到結果:232表示行號,linux/in.h表示所在文件夾
vi linux/in.h +232
這個命令是直接打開定位到該文件的232行。
- linux查看端口號占用命令-netstat
netstat -pan | grep 5623
#其中5623位端口號
如圖:可以看到占用該端口號的進程,并且可以利用ps指令找到程序名稱。
判斷socket有沒有與客戶端斷開連接
本文參照博客:socket編程