每一個網絡應用都是基於客戶端一服務器模型的
本地的進程間通訊(IPC)有不少種方式,但能夠總結爲下面4類:html
網絡中進程之間如何通訊?首要解決的問題是如何惟一標識一個進程,不然通訊無從談起!在本地能夠經過進程PID來惟一標識一個進程,可是在網絡中這是行不通的。其實TCP/IP協議族已經幫咱們解決了這個問題,網絡層的「ip地址」能夠惟一標識網絡中的主機,而傳輸層的「協議+端口」能夠惟一標識主機中的應用程序(進程)。這樣利用三元組(ip地址,協議,端口)就能夠標識網絡的進程了,網絡中的進程通訊就能夠利用這個標誌與其它進程進行交互。linux
使用TCP/IP協議的應用程序一般採用應用編程接口:UNIXBSD的套接字(socket)和UNIX System V的TLI(已經被淘汰),來實現網絡進程之間的通訊。就目前而言,幾乎全部的應用程序都是採用socket,而如今又是網絡時代,網絡中進程通訊是無處不在,這就是我爲何說「一切皆socket」。git
額web
int socket(int domain, int type, int protocol);
socket函數對應於普通文件的打開操做。普通文件的打開操做返回一個文件描述字,而socket()用於建立一個socket描述符(socket descriptor)
,它惟一標識一個socket。這個socket描述字跟文件描述字同樣,後續的操做都有用到它,把它做爲參數,經過它來進行一些讀寫操做。編程
正如能夠給fopen的傳入不一樣參數值,以打開不一樣的文件。建立socket的時候,也能夠指定不一樣的參數建立不一樣的socket描述符,socket函數的三個參數分別爲:瀏覽器
domain:即協議域,又稱爲協議族(family)。經常使用的協議族有,AF_INET、AF_INET六、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE
等等。協議族決定了socket的地址類型,在通訊中必須採用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名做爲地址。安全
type:指定socket類型。經常使用的socket類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的類型有哪些?)。服務器
protocol:故名思意,就是指定協議。經常使用的協議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議(這個協議我將會單獨開篇討論!)。網絡
注意:並非上面的type和protocol能夠隨意組合的,如SOCK_STREAM不能夠跟IPPROTO_UDP組合
。當protocol爲0時,會自動選擇type類型對應的默認協議。多線程
當咱們調用socket建立一個socket時,返回的socket描述字它存在於協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。若是想要給它賦值一個地址,就必須調用bind()函數,不然就當調用connect()、listen()時系統會自動隨機分配一個端口。
3.二、bind()函數
正如上面所說bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數的三個參數分別爲:
sockfd:即socket描述字,它是經過socket()函數建立了,惟一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。
addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址建立socket時的地址協議族的不一樣而不一樣,如ipv4對應的是:
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* 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 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
a) Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。 b) Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。
因此:在將一個地址綁定到socket的時候,請先將主機字節序轉換成爲網絡字節序,而不要假定主機字節序跟網絡字節序同樣使用的是Big-Endian。因爲這個問題曾引起過血案!公司項目代碼中因爲存在這個問題,致使了不少莫名其妙的問題,因此請謹記對主機字節序不要作任何假定,務必將其轉化爲網絡字節序再賦給socket。
若是做爲一個服務器,在調用socket()、bind()以後就會調用listen()來監聽這個socket,若是客戶端這時調用connect()發出鏈接請求,服務器端就會接收到這個請求。
int listen(int sockfd, int backlog); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函數的第一個參數即爲要監聽的socket描述字,第二個參數爲相應socket能夠
排隊的最大鏈接個數。socket()函數建立的socket默認是一個主動類型的,listen函數將socket變爲被動類型的,等待客戶的鏈接請求。
connect函數的第一個參數即爲客戶端的socket描述字,第二參數爲服務器的socket地址,第三個參數爲socket地址的長度。客戶端經過調用connect函數來創建與TCP服務器的鏈接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函數的第一個參數爲服務器的socket描述字,第二個參數爲指向struct sockaddr *的指針,用於返回客戶端的協議地址,第三個參數爲協議地址的長度。若是accpet成功,那麼其返回值是由內核自動生成的一個全新的描述字,表明與返回客戶的TCP鏈接。
注意:accept的第一個參數爲服務器的socket描述字,是服務器開始調用socket()函數生成的,稱爲監聽socket描述字;而accept函數返回的是已鏈接的socket描述字。一個服務器一般一般僅僅只建立一個監聽socket描述字,它在該服務器的生命週期內一直存在。內核爲每一個由服務器進程接受的客戶鏈接建立了一個已鏈接socket描述字,當服務器完成了對某個客戶的服務,相應的已鏈接socket描述字就被關閉。
萬事具有隻欠東風,至此服務器與客戶已經創建好鏈接了。能夠調用網絡I/O進行讀寫操做了,即實現了網咯中不一樣進程之間的通訊!網絡I/O操做有下面幾組:
#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);
將buf中的nbytes字節內容寫入文件描述符fd.成功時返回寫的字節數。失敗時返回-1,並設置errno變量。 在網絡程序中,當咱們向套接字文件描述符寫時有倆種可能。1)write的返回值大於0,表示寫了部分或者是所有的數據。2)返回的值小於0,此時出現了錯誤。咱們要根據錯誤類型來處理。若是錯誤爲EINTR表示在寫的時候出現了中斷錯誤。若是爲EPIPE表示網絡鏈接出現了問題(對方已經關閉了鏈接)。
其它的我就不一一介紹這幾對I/O函數了,具體參見man文檔或者baidu、Google,下面的例子中將使用到send/recv。
在服務器與客戶端創建鏈接以後,會進行一些讀寫操做,完成了讀寫操做就要關閉相應的socket描述字,比如操做完打開的文件要調用fclose關閉打開的文件。
#include <unistd.h> int close(int fd);
close一個TCP socket的缺省行爲時把該socket標記爲以關閉,而後當即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再做爲read或write的第一個參數。
注意:close操做只是使相應socket描述字的引用計數-1,只有當引用計數爲0的時候,纔會觸發TCP客戶端向服務器發送終止鏈接請求。
咱們知道tcp創建鏈接要進行「三次握手」,即交換三個分組。大體流程以下:
[x] 從圖中能夠看出,當客戶端調用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返回,至此三次握手完畢,鏈接創建。
總結:客戶端的connect在三次握手的第二個次返回,而服務器端的accept在三次握手的第三次返回。
上面介紹了socket中TCP的三次握手創建過程,及其涉及的socket函數。如今咱們介紹socket中的四次握手釋放鏈接的過程,請看下圖:
接收到這個FIN的源發送端TCP對它進行確認。
#include "stdafx.h" #include <winsock2.h> #include <stdio.h> #include<stdlib.h> #pragma comment(lib,"ws2_32.lib") int main(int argc, char* argv[]) { int i; WORD sockVersion = MAKEWORD(2,2); WSADATA data; if(WSAStartup(sockVersion, &data) != 0) { return 0; } SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(sclient == INVALID_SOCKET) { printf("invalid socket !"); return 0; } struct sockaddr_in serAddr; //地址結構體 serAddr.sin_family = AF_INET; serAddr.sin_port = htons(20155202); serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); if (connect(sclient, (struct sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR) { printf("connect error !"); closesocket(sclient); return 0; } int ret=0; char sendData[255]; char recData[255]; printf("你好,TCP服務端,我是客戶端!\n"); while(ret!=-1) { gets(sendData); send(sclient, sendData, strlen(sendData), 0); //發送 ret = recv(sclient, recData, 255, 0); //接收 if(ret > 0) { printf("server : "); recData[ret] = 0x00; //打印 puts(recData); } } WSACleanup(); return 0; }
#include "stdafx.h" #include <stdio.h> #include <winsock2.h> #include<stdlib.h> #pragma comment(lib,"ws2_32.lib") int main(int argc, char* argv[]) { //初始化WSA WORD sockVersion = MAKEWORD(2,2); WSADATA wsaData; if(WSAStartup(sockVersion, &wsaData)!=0) { return 0; } //建立套接字 SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(slisten == INVALID_SOCKET) { printf("socket error !"); return 0; } //綁定IP和端口 struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(20155202); sin.sin_addr.S_un.S_addr = INADDR_ANY; if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR) { printf("bind error !"); } //開始監聽 if(listen(slisten, 5) == SOCKET_ERROR) { printf("listen error !"); return 0; } //循環接收數據 SOCKET sClient; struct sockaddr_in remoteAddr; int nAddrlen = sizeof(remoteAddr); char revData[255]; while (1) { printf(" 正在鏈接客戶端...\n"); sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen); if(sClient == INVALID_SOCKET) { printf("accept error !"); continue; } printf("接受到一個鏈接:%s \r\n", inet_ntoa(remoteAddr.sin_addr)); int ret=0; char sendData[255]; while(ret!=-1) { //接收數據 ret = recv(sClient, revData, 255, 0); if(ret > 0) { revData[ret] = NULL; printf("Client : "); puts(revData); } //發送數據 gets(sendData); send(sClient, sendData, strlen(sendData), 0); } } closesocket(slisten); WSACleanup(); return 0; }
#include <stdio.h> #include <stdlib.h> #include <WinSock2.h> #define BUF_SIZE 1024 int main(){ char filename[100] = {0}; //文件名 WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); SOCKET sock = socket(AF_INET, SOCK_STREAM,0); struct sockaddr_in sockAddr; sockAddr.sin_family = AF_INET; sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); sockAddr.sin_port = htons(5202); printf("你好服務器,我是客戶端\n"); connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)); printf("\n服務器像您發送文件請求\n\n您想要接收文件保存到 : \n "); //輸入文件名,看文件是否能建立成功 gets(filename); FILE *fp = fopen(filename, "wb"); //以二進制方式打開文件 if(fp == NULL){ printf(" 建立文件失敗,請按任意鍵繼續 !\n"); system("pause"); exit(0); } //循環接收數據,直到文件傳輸完畢 char buffer[BUF_SIZE] = {0}; //文件緩衝區 int zishu; printf("\n正在接收寫入數據\n"); while( (zishu = recv(sock, buffer, BUF_SIZE, 0)) > 0 ){ fwrite(buffer, zishu, 1, fp); } puts("\n文件接收完畢\n"); //文件接收完畢後直接關閉套接字,無需調用shutdown() fclose(fp); closesocket(sock); WSACleanup(); system("pause"); return 0; }
#include <stdio.h> #include <stdlib.h> #include <winsock2.h> #define BUF_SIZE 10240 int main(){ //初始化WSA WSADATA wsaData; WSAStartup( MAKEWORD(2, 2), &wsaData); SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in sockAddr; sockAddr.sin_family = AF_INET; sockAddr.sin_addr.s_addr = INADDR_ANY; sockAddr.sin_port = htons(5202); bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)); listen(servSock, 20); SOCKADDR clntAddr; int nSize = sizeof(SOCKADDR); printf(" 服務器20155202 等待客戶端鏈接...\n"); SOCKET clntSock = accept(servSock, &clntAddr, &nSize); printf("\n接收到客戶端來電\n"); //先檢查文件是否存在 char filename[BUF_SIZE]; //文件名 printf("\n輸入你想傳送的文件名\n"); gets(filename); FILE *fp = fopen(filename, "rb"); if(fp == NULL){ printf("\n沒那個文件!\n"); system("pause"); exit(0); } printf("\n正在上傳至服務器\n"); //循環發送數據,直到文件結尾 char buffer[BUF_SIZE] = {0}; //緩衝區 int zishu; while( (zishu = fread(buffer, 1, BUF_SIZE, fp)) > 0 ){ send(clntSock, buffer, zishu, 0); } shutdown(clntSock, SD_SEND); //文件讀取完畢,斷開輸出流 printf("\n正在上傳文件至客戶端\n"); recv(clntSock, buffer, BUF_SIZE, 0); printf("\文件發送完畢\n\n"); fclose(fp); closesocket(clntSock); closesocket(servSock); WSACleanup(); system("pause"); return 0; }
問題1:多線程編譯時候線程間工做時間順序?還有pthread_join
函數中*retval指針返回值是什麼?
測試代碼以下:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #define NLOOP 50 int counter; void *doit( void * ); int main(int argc, char **argv) { pthread_t tidA, tidB; pthread_create( &tidA ,NULL, &doit,(void *)"衛彥忻\n"); pthread_create( &tidB ,NULL, &doit,(void *)"張旭"); pthread_join( tidA, NULL ); pthread_join( tidB, NULL ); return 0; } void * doit( void * vptr) { int i, val; for ( i=0; i<NLOOP; i++ ) { //val = counter++; //printf("%x: %d \n", (unsigned int) pthread_self(), val + 1); //counter = val + 1; printf("%s",vptr); } }
我結對的搭檔:呂宇軒 20155239
他的問題:
sudo apt-get install bison flex
繼續輸入:
sudo apt-get install tcl8.5-dev tk8.5-dev tcl8.5 tk8.5
5、編譯
./psim -t -g ../y86-code/asum.yo
進入Y86-code 測試一下是否可使用
perfect,能夠了
學習了網絡編程讓我知道了客戶端和服務器之間的交互的具體步驟,也讓我對各種聊天軟件有了更加深入的瞭解。
代碼行數(新增/累積) | 博客量(新增/累積) | 學習時間(新增/累積) | 重要成長 | |
---|---|---|---|---|
目標 | 5000行 | 30篇 | 400小時 | |
第14周 | 270/200 | 3/2 | 33/20 |
嘗試一下記錄「計劃學習時間」和「實際學習時間」,到期末看看能不能改進本身的計劃能力。這個工做學習中很重要,也頗有用。
耗時估計的公式
:Y=X+X/N ,Y=X-X/N,訓練次數多了,X、Y就接近了。
(有空多看看現代軟件工程 課件
軟件工程師能力自我評價表)