{%hackmd bxsQAKRrSe-0K7dbTGPINQ %} ## Network Programming ### @CS.NCTU #### Lecture 5: Client/Server Paradigms #### Instructor: Kate Ching-Ju Lin (林靖茹) <!-- Put the link to this slide here so people can follow --> https://hackmd.io/@wbjfRtFvQ96uXiHjQ0AvgA/HkQP7fqkY --- ## Client/Server Paradigms * Basic functions * Iterative, connectionless servers * Iterative, connection-oriented servers * Concurrent, connection-oriented servers * Single-process, concurrent servers * Multi-protocol servers * Multi-service servers * Concurrency in clients Note: 從homos那本教科書擷取過來。一個chapter就是一個單元的事情。把可能的幾種寫法都寫在書裡面,每個paradigm都寫在書裡面變成一個chapter,都找得到。以後想寫怎樣的應用架構,就去看那個chapter就好,依照城市的範例來延伸就可以。範例程式的coding style也都還ok,很多甚至可以直接採用。 上週的範例就比較多問題,有時候效率會比較差。現在如果要用範例當然java python會快一點。但如果享用c++就可以用書上的範例延伸 --- ## Basic Functions ---- ### connectTCP and connectUDP ```c++= int connectTCP( host, service ) char *host; /* name of host to which connection is desired */ char *service; /* service associated with the desired port */ { return connectsock( host, service, "tcp"); } int connectUDP( host, service ) char *host; /* name of host to which connection is desired */ char *service; /* service associated with the desired port */ { return connectsock(host, service, "udp"); } ``` Note: 這邊從client講起。比方我們有一個client要連到server,client用prot來當出入口,client和server都有process要互相溝通。client想要建一個socket連到server。 如果今天用java or python,原則上觀念跟這邊是差不多的。 先看一下connect TCP,對client來說,當我們要連過去的時候要要知道什麼東西呢? Connection的ID包括: (IP_s, P_s, IP_d, P_d, protocol)。protocol就是TCP or UDP。TCP會做congestion control / flow control。UDP就只是raw transmission。 所以client要指定server的IP和port。比方http的port就是80,還要指定用TCP or UDP。 你可能會問為什麼不用指定IP_s P_s? IP_s其實就是自己的IP,不用指定,那自己的port呢? 因為我們會呼叫bind, 如果沒有指定port ID, 系統就會隨機指定一個。所以這個函式就是給定host和port就好。 以後如果看到文獻,連到哪個port其實就是用他對應的服務,所以我們有時候也會寫成server,但其實意思就是對方的窗口, port, 的意思。 那這個function書上都有提供範例。 如果從系統的角度,比方我們要用一個服務 telnet server http 這邊可以寫成 telnet www.cs.xx 80 或是 telnet 140.113.xxx.xxx 80 這些程式 connect sock connect TCP就是提供給你一些範例,如果我們叫telnet,那s就是arg1 port就是arg2,用這個程式就可以call過去。 ---- ### Connectsock -- (1) ```c++= int connectsock( host, service, protocol ) char *host; /* name of host to which connection is desired */ char *service; /* service associated with the desired port */ char *protocol; /* name of protocol to use ("tcp" or "udp") */ { struct hostent *phe; /* pointer to host information entry */ struct servent *pse; /* pointer to service information entry */ struct protoent *ppe; /* pointer to protocol information entry*/ struct sockaddr_in sin; /* an Internet endpoint address */ int s, type; /* socket descriptor and socket type */ bzero((char *)&sin, sizeof(sin)); sin.sin_family = AF_INET; ``` Note: 這邊的參數都可以不用管 因為我們要連到server端,因為我們要準備一個socket,所以要告訴他 family,預設是AF_INET, ---- ### Connectsock -- (2) ``` c++= /* Map service name to port number */ if ( pse = getservbyname(service, protocol) ) sin.sin_port = pse->s_port; else if ( (sin.sin_port = htons((u_short)atoi(service))) == 0 ) errexit("can't get \"%s\" service entry\n", service); /* Map host name to IP address, allowing for dotted decimal */ if ( phe = gethostbyname(host) ) bcopy(phe->h_addr, (char *) &sin.sin_addr, phe->h_length); else if ( (sin.sin_addr.s_addr = inet_addr(host)) == INADDR_NONE ) errexit("can't get \"%s\" host entry\n", host); ``` Note: 接下來還要填兩個參數,一個是service name,一個是http或是ftp或是echo... 因為我們要得到這些服務的number,所以要得到他的port。一班常見的網路服務都有公定的port,通常是<1000。比方http就是80, ftp=23, telnet =21。如果要去改服務的port也可以,但是就是server要去指定要用別的port去接這樣的service。 w6-1: 18:27 Getservicebyname就是跟系統要一個資料結構,會有一個service列表,記錄每個service的port,所以系統就會回傳你要查的服務的port。所以如果我們要用ftp這個服務,就會回報ftp的port。 如果找不到 pse就會=0,比方系統沒有這個服務,或是你寫錯字了 ftp寫成fftp,return值就會變成null 就會接下去檢查。如果/etc找不到,就假設你現在給的service名稱是一個number,不是字串,我就用這個number去轉換,用atoi把你給的number轉換成port的integer格式。再把轉換過後的port number存入port裏面。 這邊大家稍微注意一下,我們還是有htons的轉換,但是getservicebyname沒有,因為getservicebyname這個函式吐出來就已經是port的格式了,所以不用轉換。 Port好的那接下來就是要設定IP, 接下來也適用gethostbyname,這邊的input我們可以直接用hostname,也可以用IP 像是140.113.xx 如果不是hostname的話,那我們就假設是4個8byte的IP address,就會用inet_addr去轉換 ---- ### Services ###### example $\textrm{/etc/services}$ services ![](https://i.imgur.com/DR3k4Zy.png) Note: 問題是系統怎麼知道不同service的prot? 有些port是有預設值,在unix裏面. /etc/services有這樣一個檔案,就會記錄每個service的port是多少。以ftp為例,我們就可以抓到資料結構裡面的ftp的port 21,就會return回來。所以如果你們有裝linux,可以去/etc下面去找,去看看你現在系統裡的service這個資料結構。 ---- ### Hosts ![](https://i.imgur.com/VvFQcYl.png) Note: Gethostbyname也是一樣,會用/etc/hosts這個檔案去查,如果有在這個檔案裡面就查得到,找不到就return null。 那也是一樣return的時候也是轉換好了,所以不用在用anet_addr轉成source address格式。 這邊大家可能會覺得很怪 hosts裡面怎麼可能會記錄那麼多hostname,是因為以前的server很少,所以把少數已知的server放在hosts就好。但現在不同的server這麼多,不太可能會把所有的server記在這個檔案裡面,所以這個已經是舊的做法。 所以現在很多都是要用DNS 找DNS server用hostname直接去查server IP。 所以這邊要改成call DNS server,不過因為查IP要時間,如果這邊要去查DNS,就會產生一些delay,程式會卡在這邊。 有時候我們感覺不動這個問題的嚴重性是因為我們假設DNS的timeout都很短,如果超過一段時間就會放棄,當作失敗。所以這邊要注意可能會有一個小小的delay。這個delay就蠻重要的,因為如果有一個小小的delay,比方我們再寫一個遊戲,如果這邊有個小delay,整個系統就會有時候卡卡的。 所以這邊我們如果有問題,有時候就會寫一個callback function,如果發生問題的話,看要怎麼處理。這是大家要注意的。 所以如果gethostbyname有找到IP,就會寫進去IP,沒有的話就會把你給的input轉換成addr的格式,塞到IP_s。 ---- ### Connectsock -- (3) ``` c++= /* Map protocol name to protocol number */ if ( (ppe = getprotobyname(protocol)) == 0) errexit("can't get \"%s\" protocol entry\n", protocol); /* Use protocol to choose a socket type */ if (strcmp(protocol, "udp”) == 0) type = SOCK_DGRAM; else type = SOCK_STREAM; /* Allocate a socket */ s = socket(PF_INET, type, ppe->p_proto); if (s < 0) errexit("can't create socket: %s\n", sys_errlist[errno]); ``` Note: 抓到了之後,socket address已經抓到,接下來處理protocol,不是tcp就是udp,一樣,就會去/etc/protocols來找,去掃描每一行,找得到就會回傳protocol ID,找不到就會回傳null。 根據我們抓到的protocol,如果是tcp或是udp,就把type設定成指定protocol的data type,看是datagram或是streaming。 這些都準備好了之後,就可以產生socket。 如果第三個參數填0,系統就會依照你給的type,去判斷你要的是tcp or udp。 ---- ### Protocol List ![](https://i.imgur.com/HKVqUAt.png) ---- ### Connectsock -- (4) ```c++= /* Connect the socket */ if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) errexit("can't connect to %s.%s: %s\n", host, service, sys_errlist[errno]); return s; } ``` Note: Socket 資料結構生出來之後就可以call connect去建立連線,練到server去。client這樣就把connection建立起來了。抓到的socket ID比方是3,以後就用這個socket來做處理。 之後你們的作業可能會有blocking,有時候很多情況,比方我們要read or write,希望不要背block住,如果要呼叫non blocking就要在connect()之前來處理。 ---- ### TCPdaytime.c -- (1) ``` c++= int main(argc, argv) int argc; char *argv[]; { char *host = "localhost"; /* host to use if none supplied */ char *service = "daytime"; /* default service port */ switch (argc) { case 1: host = "localhost"; break; case 3: service = argv[2]; /* FALL THROUGH */ case 2: host = argv[1]; break; default: fprintf(stderr, "usage: TCPdaytime [host [port]]\n"); exit(1); } TCPdaytime(host, service); exit(0); } ``` Note: 來看一些例子。這個例子是daytime。這幾個例子的安排都蠻好的,都先安排幾個小的operation,比如像data time要去問time server,來去replay一個server,就是一個query回來就結束了。是一個很lightweight的service。所以一開始用這個範例學網路程式設計比較輕鬆一點 這個城市compile之後 %tcpdaytime server_name protocol 所以argv就是指向三個streams的陣列,by default, host就是cohost, server就是daytime這個port 這邊這個字串是到時候要用來去查 /etc/services的字串 他的做法就很簡單,如果只有一個參數,那第一個就是localhost,如果有三個參數,argv[2]就是service name, ---- ### TCPdaytime.c -- (2) ```c++= TCPdaytime(host, service) char *host; char *service; { char buf[LINELEN+1]; /* buffer for one line of text */ int s, n; /* socket, read count */ s = connectTCP(host, service); while( (n = read(s, buf, LINELEN)) > 0) { buf[n] = '\0'; /* insure null-terminated */ (void) fputs( buf, stdout ); } } ``` Note: 這個寫法,其實有一些錯誤的示範,是蠻老舊的寫法,第一個問題是 1-> 3-> 2就沒照順序,應該要照順序。如果有在做compiler的話,也是照字元 a->b-> c。寫文章也是,如果要寫論文,如果要討論三個東西,如果你先寫b再寫c再寫a,也惠很惱人。如果順序不對,developper要去找就很難找,如果要共同開發就會很不方便。大家不要覺得這東西沒什麼,但是太隨心所欲,可能會害別人花了更多時間在trace code。以後在公司,code base會很大,都是一群人共同開發,不按照大家的默契會讓大家很困擾。搞不好之後你自己回來看也看不懂。幫助別人就是幫助自己! 他之所以這樣寫是為什麼,是因為會改的話通常就會給兩個參數,就直接設定,好像效率好一點。但是現在compiler都很厲害了,compiler都會抓得到,他底下就會自己處理,把他compiler成比較有效率的城市。所以寫程式就寫一個比較好看懂的就好了,不用為了效率去換這些順序。 ---- ### TCPdaytime.c – (2) ``` c++= int main(argc, argv) int argc; char *argv[]; { char *host = "localhost"; /* host to use if none supplied */ char *service = "time"; /* default service name */ time_t now; /* 32-bit integer to hold time */ int s, n; /* socket descriptor, read count*/ ... See TCPdaytime.c -- (2) ... s = connectUDP(host, service); (void) write(s, MSG, strlen(MSG)); /* Read the time */ n = read(s, (char *)&now, sizeof(now)); if (n < 0) errexit("read failed: %s\n", sys_errlist[errno]); now = ntohl((u_long)now); /* put in host byte order */ now -= UNIXEPOCH; /* convert UCT to UNIX epoch */ printf("%s", ctime(&now)); exit(0); } ``` Note: 連到server之後就可以去buffer把東西read出來,如果讀出來的直都>0,就表示還沒讀完,就要繼續讀。如果讀到0就表示connection close了。 ---- ### UDPtime.c -- (1) ``` c++= int main(argc, argv) int argc; char *argv[]; { char *host = "localhost"; /* host to use if none supplied */ char *service = "time"; /* default service name */ time_t now; /* 32-bit integer to hold time */ int s, n; /* socket descriptor, read count*/ switch (argc) { ... See TCPdaytime.c -- (2) ... } ``` Note: 接下來講UDP,剛剛的TCP是Daytime,現在UDP變成用 % UDPtime Server time 所以跟剛剛的一樣 switch就不特別講,一樣是連到Server, ---- ### UDPtime.c – (2) ``` c++ s = connectUDP(host, service); (void) write(s, MSG, strlen(MSG)); /* Read the time */ n = read(s, (char *)&now, sizeof(now)); if (n < 0) errexit("read failed: %s\n", sys_errlist[errno]); now = ntohl((u_long)now); /* put in host byte order */ now -= UNIXEPOCH; /* convert UCT to UNIX epoch */ printf("%s", ctime(&now)); exit(0); } ``` Note: [W6-2 1:50] ConnectUDP和connectTCP類似,一樣是從一個port連到time server。 s=connect 連上去之後就會return socket。 UDP的write和read就跟TCP的send和recv一樣,UDP可以透過write把線接起來,用一個封包叫MSG送在這條線上。 這邊的MSG其實一開始是一個Junk message,因為對UDP來說如果一開始不送一個junk message,server不知道有人傳東西給我。 Server要收到這個Junk message才知道要回傳回去給client。 傳這個空的訊息過去其實是為了要跟Server說我的地址在哪,Server收到這個空的包裹才知道Client是從哪個位址連到我這邊來。 所以read讀回來的時候就會讀回Server回傳的東西 [W6-2 5:50] 所以client就要說明now要在哪個位址去接,還要指定要讀多少,讀太多我也不要。讀回來就會告訴我n讀到了幾個byte。 Now讀完如果正常沒有錯誤,就可以把獨到的資料,用ntohl去轉格式,把網路格式轉到標準格式。 之前TCP格式不一樣,現在變成long的格式,圖回來變成真的now,讀完就可以印出來,所以UDP最重要的事要做一個空的包裹 --- ## Iterative Servers ---- ### passiveTCP and passiveUDP ``` c++= int passiveTCP( service, qlen ) char *service; /* service associated with the desired port */ int qlen; /* maximum server request queue length */ { return passivesock(service, "tcp", qlen); } int passiveUDP( service ) char *service; /* service associated with the desired port */ { return passivesock(service, "udp", 0); } ``` Note: Client端connect結束後,就要看server端。Server端比較複雜一點,有master socket還有slave socket。Server listen to某個port的話,其實也算是一個connection,只是比較像是半個connection [W6-29:40] 只是它只有三個input (IPs, Ps, TCP/UDP)。所以對server來說,要做passive TCP,是有人來的時候我才會開起來,不會一直開著。有TCP的queue length還有UDP的 server。 Queue length就是控制連進來可以有幾個人,在設計這個的時候為了完整起見,會把qlen參數寫進去,但現在都是用預設值不用指定了。 所以真正個資訊只要 (*, Ps) *意思是隨便從哪張卡進去都可以。但是port要指定。 那如果是passiveUDP的話,就設定UDP,最後就灌0 意思是by default ---- ### Passivesock – (1) ```c++= int passivesock( service, protocol, qlen ) char *service; /* service associated with the desired port */ char *protocol; /* name of protocol to use ("tcp" or "udp") */ int qlen; /* maximum length of the server request queue */ { struct servent *pse; /* pointer to service information entry */ struct protoent *ppe; /* pointer to protocol information entry*/ struct sockaddr_in sin; /* an Internet endpoint address */ int s, type; /* socket descriptor and socket type */ bzero((char *)&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; /* Map service name to port number */ if ( pse = getservbyname(service, protocol) ) sin.sin_port = htons(ntohs((u_short)pse->s_port) + portbase); else if ( (sin.sin_port = htons((u_short)atoi(service))) == 0 ) errexit("can't get \"%s\" service entry\n", service); /* Map protocol name to protocol number */ if ( (ppe = getprotobyname(protocol)) == 0) errexit("can't get \"%s\" protocol entry\n", protocol); ``` Note: Passive socket如果要連到server要有一個socket address,一樣要有port number IP, family family就是AF_INET,address是any (*) INADDR_ANY,接下來只剩下port還沒決定。 那port怎麼連呢? 先用getservbyname() 跟前面一樣先拿到protocol, 也一樣用getprotobyname拿到protocol name,也是UDP或是TCP。最後一段也跟之前一樣,看要是SOCK_DGRAM還是SOCK_STREAM ---- ### Passivesock – (2) ```c++=24 /* Use protocol to choose a socket type */ if (strcmp(protocol, "udp") == 0) type = SOCK_DGRAM; else type = SOCK_STREAM; /* Allocate a socket */ s = socket(PF_INET, type, ppe->p_proto); if (s < 0) errexit("can't create socket: %s\n", sys_errlist[errno]); /* Bind the socket */ if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) errexit("can't bind to %s port: %s\n", service, sys_errlist[errno]); if (type == SOCK_STREAM && listen(s, qlen) < 0) errexit("can't listen on %s port: %s\n", service, sys_errlist[errno]); return s; } ``` Note: 一樣要產生socket,到這邊跟前面幾乎一模一樣。在server端呢一樣會有一個master socket,專門來做listen,如果client進來就會找另一個服務人員去處理這個client。 做法就跟之前標準程序一樣,先bind到某一個port,就會請master socket先做到某個位子,接下來就要打開服務的窗口去listern,開始營業,開始後就可以return socket s。另一個要注意的地方是,這邊的寫法對軟體來說不是很好,是另一個錯誤的示範,他如果是UDP的話,並沒有listen這個動作,這個code這樣寫意思是說,如果是SOCK_STREAM,就是說是TCP,第一個條件就會是true,再去做listen這個動作。 (A && B) A是對的話才會去做B,A如果不對,就不會去做B,所以如果是UDP就不會去listen。這樣寫是比較簡潔,但是developer會比較不容易看到listen藏在這邊。 這個如果是在公司開發程式,這樣寫就有點容易被誤會,不仔細看會以為listen永遠會被執行 If (type == SOCK_SDGRAM { //tcp Listen() } 這樣寫會比較清楚 寫程式也要盡量少寫hidden module 不要自作聰明 另一個例子 swap Tmp = x X = y Y – tmp 有人自作聰明 `X ^=y ^= x ^= y` 連我們看到都有點怕怕的...還要花時間去verify一次到底對不對,反而搞死大家 If `(A&&B) if not( (notA) || (notB))` straightforward比較好 --- ### Iterative, Connectionless Servers ![](https://i.imgur.com/NEsO5fr.png) ###### Example:UDPtimed.c (next slides) Note: Server的話他先來講connectionless server ---- ### UDPtimed.c – (1) ```c++= int main(argc, argv) int argc; char *argv[]; { struct sockaddr_in fsin; /* the from address of a client */ char *service = "time"; /* service name or port number */ char buf[1]; /* "input" buffer; any size > 0 */ int sock; /* server socket */ time_t now; /* current time */ int alen; /* from-address length */ switch (argc) { case 1: break; case 2: service = argv[1]; break; default: errexit("usage: UDPtimed [port]\n"); } ``` Note: 這個server是UDP的,先設定好只有一個argument ---- ### UDPtimed.c – (2) ```c++= sock = passiveUDP(service); while (1) { alen = sizeof(fsin); if (recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)&fsin, &alen) < 0) errexit("recvfrom: %s\n", sys_errlist[errno]); (void) time(&now); now = htonl((u_long)(now + UNIXEPOCH)); (void) sendto(sock, (char *)&now, sizeof(now), 0, (struct sockaddr) &fsin, sizeof(fsin)); } } ``` Note: 因為他是UDP,所以call passiveUDP。進入while loop之後,一直在那邊等,等著收packet,這邊要用recvfrom不能用read,不然讀不到socket address。 [w6-2: 35:35] 這個除了除近來之外,也會準備好一個sock資料結構,把收到的包裹的地址收到scoekt的fsin。收到服務根本不管包裹裡面的累戎,重點是要知道client是從哪個address哪個port過來,要把它存到fsin。 收到之後就會知道他需時間服務,就去呼叫system call,把時間讀出來存到now這個buffer裏面,再把這個buffer格式用htonl把host轉乘network格式。做好這個now就可以傳回給client,傳回去的地址就是剛剛空的包裹的地址,程式就結束了。如果對照剛剛的UDPTime client,client就會收到這個帶有時間的包裹。 --- ### Iterative, Connection-Oriented Servers <p align="center"> <img src="https://i.imgur.com/wRr1UtP.png" width="400"> </p> Note: TCP的server,叫iterative, connection-oriented。所以當client建一個connection過來的時候,server會accept。因為time server是一個很lightweight的server,本身connection過來就是一個訊息,所以server收到訊息就知道client是要來問時間,所以client也不用在建立連線後再多送其他訊息,server可以直接回傳時間給他。 ###### Example: TCPtimed.c (next slides) ---- ### TCPdaytimed.c – (1) ```c++= int main(argc, argv) int argc; char *argv[]; { struct sockaddr_in fsin; /* the from address of a client */ char *service = "daytime"; /* service name or port number */ int msock, ssock; /* master & slave sockets */ int alen; /* from-address length */ switch (argc) { case 1: break; case 2: service = argv[1]; break; default: errexit("usage: TCPdaytimed [port]\n"); } ``` ---- ### TCPdaytimed.c – (2) ```c++= msock = passiveTCP(service, QLEN); while (1) { ssock = accept(msock, (struct sockaddr *) &fsin, &alen); if (ssock < 0) errexit("acceptfailed:%s\n",sys_errlist[errno]); (void) TCPdaytimed(ssock); (void) close(ssock); } } ``` Note: 所以一開始的時候,這個程式會run成一個deamon,去建立passiveTCP。如果有任何一個connection進來就會accept,裡面有兩個參數,可以抓出是誰連進來,這個程式目前沒有用alen,但其他應用可能會知道。 接著就會產生一個slave socket,如果這是一個slave socket就會再去call TCPdaytimed,回傳時間後就把自己close掉。 [w6-2 42:00] 在這個case結束後,就會把slave socket (fd[4])關掉。 ---- ### TCPdaytimed.c – (3) ```c++= /*------------------------------------------------------------------------ * TCPdaytimed - do TCP DAYTIME protocol *------------------------------------------------------------------------ */ int TCPdaytimed(fd) int fd; { char *pts; /* pointer to time string */ time_t now; /* current time */ char *ctime(); (void) time(&now); pts = ctime(&now); (void) write(fd, pts, strlen(pts)); return 0; } ``` Note: Fd傳進來TCPdaytime之後,就會去抓時間,取出來之後就會把時間轉成字串pts,然後把這個字串寫回去fd這個slave socket,用write寫進去buffer傳回去。 --- ## Concurrent, Connection-Oriented Servers <p align="center"> <img src="https://i.imgur.com/wRr1UtP.png" width="400"> </p> ###### Example: TCPechod.c (next slides) Note: 接下來就比較複雜,我們的二個project就會用這個model。 在更早以前的project,這是第一個project。不過後來希望大家先練習一個比較簡單的東西,就多安插了第一個project。 這個model是說如果我們想要連到遠端server,可以用 %telnet abc 7000 就可以連到遠端的shell,跟現在大家用的ssh很類似,只是現在ssh有加密,telnet沒有加密。如果沒有用ssh,做出第二個project就會發現功力大增。 這個case跟上一個case原則上長的差不多,有一個master,每次一個client進來就有一個slave去handle他。 ---- ### TCPechod.c – (1) ``` c++= int main(argc, argv) int argc; char *argv[]; { char *service = "echo"; /* service name or port number */ struct sockaddr_in fsin; /* the address of a client */ int alen; /* length of client's address */ int msock; /* master server socket */ int ssock; /* slave server socket */ switch (argc) { case 1: break; case 2: service = argv[1]; break; default: errexit("usage: TCPechod [port]\n"); } ``` Note: 一開始的main就跟之前差不多。service的話,就是進去echo daemon,也可以改成別的比方http, ftp, dns。這是unix的echo這隻程式。 ---- ### TCPechod.c – (2) ```c++=20 msock = passiveTCP(service, QLEN); (void) signal(SIGCHLD, reaper); while (1) { alen = sizeof(fsin); ssock = accept(msock, (struct sockaddr *) &fsin, &alen); if (ssock < 0) { if (errno == EINTR) continue; errexit("accept: %s\n", sys_errlist[errno]); } switch (fork()) { case 0: /* child */ (void) close(msock); exit(TCPechod(ssock)); default: /* parent */ (void) close(ssock); break; case -1: errexit("fork: %s\n", sys_errlist[errno]); } } } ``` Note: 前面的setting跟之前差不多,先用passiveTCP產生master socket。 [w6-3 4:25] 從fd table來看的話,fd[3]就是master socket。再下來會去叫signal,這是一個signal table的宣告,如果有SIGCHLD這個signal的話就交給reaper這個function來處理。SIGCHLD是一個child process結束的時候會發出來的signal,每次收到就會去把reaper叫起來。我們的project也會故意製造出一堆process,所以如果不去處理的話fd table就會爆掉。 在msock產生之後就會進去while loop,進去之後會先開始accept,開始收件,如果有client進來就會開始收件,對上去了之後就會create另外一個slave socket s_sock給他,利用這個socket跟遠端的client連線。msock就是fd[3], ssock是fd[4]。接下來就判斷自己是不是一個slave socket ---- ### TCPechod.c – (3) ``` c++=42 switch (fork()) { case 0: /* child */ (void) close(msock); exit(TCPechod(ssock)); default: /* parent */ (void) close(ssock); break; case -1: errexit("fork: %s\n", sys_errlist[errno]); } } } ``` Note: 再下來,用fork去產生一個child process,connection就會連到Child process,child process的fd table會整個複製一份, fd[3]是msock, fd[4]是stock。在我們還沒講到進階技巧之前,fork結束後,如果child產生出來就可以把parent關掉了。如果有error就會去印出錯誤訊息 child process除非會用到parent ,不然就可以把它關掉 (下一頁解釋TCPeachod) 跳回來之後就會ext離開這個process 大家可以想像project 1,可以產生一個自己的program % myprog 現在如果改遠端連線,就是改成連到TCPdaemon,所以結構上和project差不多。 ---- ### TCPechod.c – (4) ``` c++= int TCPechod(fd) int fd; { char buf[BUFSIZ]; int cc; while (cc = read(fd, buf, sizeof(buf))) { if (cc < 0) errexit("echo read: %s\n", sys_errlist[errno]); if (write(fd, buf, cc) < 0) errexit("echo write: %s\n", sys_errlist[errno]); } return 0; } int reaper() { union wait status; while (wait3(&status, WNOHANG, (struct rusage *)0)>= 0) /* empty */; } ``` Note: Deamon就會去產生一個buffer,每次去connection把東西讀進來,然後read完就會告訴我們讀到多少,讀到多少就寫出去多少。讀什麼就寫什麼,直到client打cntl-D就可以產生結束的signal,read獨到的直就會是0, 就會跳出read = 0 就會結束 剛剛提到結束時的signal觸發的reaper,表示有一個child要結束了,結束的時候就會發SIGCHLD訊號,進來就會去call wait。為了保險起見,就會盡可能回收,NOHANG意思是non-blocking。這個清理的動作一定要做,一定要忠實反要各種狀況,該回收就要回收,不然只要一個地方沒做對,在某個時間點一定會爆發。 --- ## Single-Process, Concurrent Servers (TCP) <p align="center"> <img src="https://i.imgur.com/oJ8dtpD.png" width="400"> </p> ###### Example: TCPmechod.c (next slides) Note: Signal processing也跟project有關,不過這稍微再更複雜一點。剛剛的case,fork之後,在echo的過程中,如果又有另一個client連進來就會再繼續生出新的slave socket。在第一個project的時候還蠻單純的,但是在有一些case,比方如果要寫聊天室,如果每一個人連近來都給他一個sock,對於這種聊天室,就不能用剛剛的寫法。之前的架構可以用在其他應用,比方shell, ssh, ftp, http都很ok,但是如果要做一個game server,這樣的結構一定不夠, [w6-3 18:30] 比方有多個client連進來,我們就會產生多個stock,one for every connection。有幾個client就有幾個process。這樣的好處是,client送什麼訊息我們就回什麼訊息。但是對聊天室就不太一樣,如果有一個訊息進來,比方説大家好,這個訊息要回傳給所有其他client,這樣就non-trial,因為很難從不同connection傳回去。所以這種應用slave不能每個都獨立。沒辦法一個一個回怎麼辦? 有幾個解決方式,可以在不同slave之間建立connection互相溝通,可是可以想像這樣也不容易,如果有N個connection就要建立N^2個connection。另一個比較簡單的方式怎麼做? 就只有一個process就好,所有client都連到同一個process,這個process收到訊息後就可以回傳給所有其他人。 ---- ### TCPmechod.c (1) ``` c++= /* TCPmechod.c - main, echo */ #include <sys/types.h> #include <sys/socket.h> #include <sys/time.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <stdio.h> #define QLEN 5 /* maximum connection queue length */ #define BUFSIZE 4096 extern int errno; int errexit(const char *format, ...); int passiveTCP(const char *service, int qlen); int echo(int fd); ``` Note: 這樣的話寫法就會跟echo有點不同,會比較複雜一點。 對這種問題來說,我們希望所有client都連過來,但是原先的function要解決這個做法會有點難度,因為最大個困難是怎麼去收訊息? 有這麼多client會送訊息進來,怎麼去收不同client的訊息,如果只寫一個read,因為他是一個blocking IO,如果read一個client,如果這個client不理我,其他client一直講話又不去處理他,就會有問題。那怎麼辦,一種方式是用non-blocking IO,就是沒讀到就出來,但是這樣還是有問題,如果一直去掃,CPU就會很累一直空轉。 那有人就說我們就稍微偷懶一點,先讀一圈如果沒收到東西,就sleep一下,再進行下一個回合,就不會操爆CPU。可是這樣如果真的有訊息進來就會有額外的delay。雖然看起來sleep沒多久,但實際上就是會影響到performance。所以都不是太好的方法。 比較好的方法是直接跟系統說,我對這幾個client的訊息都有興趣,如果有任何一個client有訊息進來都要告訴我,有點像是去deploy很多探針,偵測到任何訊息就要通知我。這個函示就是select。 (在page ) ---- ### TCPmechod.c (2) ```c++= /*------------------------------------------------------------------------ * main - Concurrent TCP server for ECHO service *------------------------------------------------------------------------*/ int main(int argc, char *argv[]) { char *service = "echo"; /* service name or port number */ struct sockaddr_in fsin; /* the from address of a client */ int msock; /* master server socket */ fd_set rfds; /* read file descriptor set */ fd_set afds; /* active file descriptor set */ int alen; /* from-address length */ int fd, nfds; switch (argc) { case 1: break; case 2: servive = argv[1]; break; default: errexit("usage: TCPmechod [port]\n"); } ``` Note: 對一個process來說,在kernel會有fd table,是一個bit vector,相當於一個file ID的vector。 ---- ### TCPmechod.c (2) ```c++=21 msock = passiveTCP(service, QLEN); nfds = getdtablesize(); FD_ZERO(&afds); FD_SET(msock, &afds); while (1) { memcpy(&rfds, &afds, sizeof(rfds)); if (select(nfds, &rfds, (fd_set *)0, (fd_set *)0, (struct timeval *)0) < 0) errexit("select: %s\n", strerror(errno)); ``` Note: Server原則上就是echo, 如果有訊息進來就會broadcast給大家,但是範例為了簡化就改用echo。 Fds的話,這邊會藏一個bit vector叫做fdset,裡面有nfsd和rfds [w6-3 29:00] Vector會有編號0, 1, 2, ...a代表active的意思, r代表我們想要對哪些socket去read,所以我們就把reds read出來,write到fd_set。但是這個vector可能很多個,所以要有一些限制,所以nfds只是在給定說我讀資料最多要用到幾個。 Nfds的直可以用getdtablesize取得。 一開始initial的afds是空的,再下來,一開始的會有一個sock,listen到指定的port。msock放在fd[3],所以一開始FD_SET就會在fd[3]也就是afds第三個格子打一個溝溝。所以一開始select的時候只有afds[3]會有溝溝。 接下來如果有client連進來,msock就會去read Read進來之後,select執行下去之後,會把afds整個copy到rfds,去當作我們的探針。 Read進來之後,select執行下去之後,會把afds整個copy到rfds,去當作我們的探針。 ---- ### TCPmechod.c (3) ```c++=33 if (FD_ISSET(msock, &rfds)) { int ssock; alen = sizeof(fsin); ssock = accept(msock, (struct sockaddr *)&fsin, &alen); if (ssock < 0) errexit("accept: %s\n", strerror(errno)); FD_SET(ssock, &afds); } for (fd=0; fd<nfds; ++fd) if (fd != msock && FD_ISSET(fd, &rfds)) if (echo(fd) == 0) { (void) close(fd); FD_CLR(fd, &afds); } } } ``` Note: client連進來之後, fd[3]就會去read,FD_ISSET就會去檢查rfds。select那一瞬間rfds的第三格的勾勾還在,所以FD_ISSET就會回傳yes, 就會進去accept這個新的connection,然後產生新的slave socket,fd[4]。因為我們希望也去聽這個新的slave socket,所以就用在用FD_SET去把afds的第四格也打勾勾,表示fd[4]也要變成active,是我們有興趣去聽的process。 下次再呼叫select的時候就會在把afds copy到rfds,這樣就會udpate探針去聽fd[3] fd[4] 下次再call select如果fd[4]沒有訊息進來, 就會把rfds第四格的勾勾去掉,表示沒有訊息 之後如果第二個client進來, FD_ISSET就會再被呼叫 把afds的第五格打勾勾,接著while loop在跑道select,就會在把afds copy到rfds 下次如果c1有訊息進來, c2沒有, 那select就會把第五格的勾勾去掉, 保留第三第四格 接著因為有訊息進來,勾勾還留著,就會去呼叫echo ---- ### TCPmechod.c (4) ```c++= /*------------------------------------------------------------------------ * echo - echo one buffer of data, returning byte count *------------------------------------------------------------------------ */ int echo(int fd) { char buf[BUFSIZE]; int cc; cc = read(fd, buf, sizeof buf); if (cc < 0) errexit("echo read: %s\n", strerror(errno)); if (cc && write(fd, buf, cc) < 0) errexit("echo write: %s\n", strerror(errno)); return cc; } ``` Note: 接著就會去這個fd把資料用read讀進來,cc是一個count,如果是不是負值,就會把獨到的東西傳回去
{"metaMigratedAt":"2023-06-16T06:09:01.826Z","metaMigratedFrom":"YAML","title":"L5_Client/Server (note-1)","breaks":true,"description":"View the slide with \"Slide Mode\".","slideOptions":"{\"spotlight\":{\"enabled\":true},\"transition\":\"fade\",\"display\":\"block\",\"slideNumber\":true,\"overview\":true,\"hideAddressBar\":true}","contributors":"[{\"id\":\"c1b8df46-d16f-43de-ae5e-21e343402f80\",\"add\":51813,\"del\":25750}]"}
    340 views