{%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

Note:
問題是系統怎麼知道不同service的prot? 有些port是有預設值,在unix裏面. /etc/services有這樣一個檔案,就會記錄每個service的port是多少。以ftp為例,我們就可以抓到資料結構裡面的ftp的port 21,就會return回來。所以如果你們有裝linux,可以去/etc下面去找,去看看你現在系統裡的service這個資料結構。
----
### Hosts

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

----
### 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

###### 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}]"}