# Socket()
流程圖:

### Socket()
使用socket(int ,int ,int )在kernel中建立一個socket,並傳回該socket的檔案描述符
```
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
```
Domain
domain定義socket要在哪個領域溝通,以下列常用
```
* (1)AF_INET IPv4因特網域 (兩台主機透過網路進行資料傳輸)
* (2)AF_INET6 IPv6因特網域 (兩台主機透過網路進行資料傳輸)
* (3)AF_UNIX Unix域 (程序與程序間的傳輸)
```
Type
定義socket傳輸手段,以下列常用
```
* (1)SOCK_STREAM 序列化的連接導向通訊。對應的protocol為TCP。
* (2)SOCK_DGRAM 提供的是一個一個的資料包(datagram)。對應的protocol為UDP
```
Protocol
設定socket的協定標準,一般來說都會設為0,讓kernel選擇type對應的默認協議。
成功產生socket時,會返回該socket的檔案描述符(socket file descriptor)我們可以透過它來操作socket。若socket創建失敗則會回傳-1。
### bind()
bind函數把一個地址族中的特定地址賦予給socket,例如AF_INET就是把一個ipv4地址和端口號組合賦予給socket。
```
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
```
sockfd
要操作的socket描述符。
addr
指定要綁定給sockfd的協議地址。這個地址根據地址協議族的不同而不同。
addrlen
對應地址的長度。
通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用於提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是為什麼通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
```
#include <sys/types.h>
#include <sys/socket.h>
#incluse <netinet/in.h>
struct sockaddr {
unsigned short sa_family; // 2 bytes address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
// IPv4 AF_INET sockets:
struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};
struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()
};
```
程序員不應操作sockaddr,sockaddr是給操作系統用的
程序員應使用sockaddr_in來表示地址,sockaddr_in區分了地址和端口,使用更方便。
### listen() connect()
作為一個server,在調用socket()、bind()之後就會調用listen()來監聽這個socket,等待客戶端的連接。
```
int listen(int sockfd, int backlog);
```
sockfd
要監聽的socket描述符。
backlog
指定監聽佇列大小。
listen():
The listen() function applies only to stream sockets. It indicates a readiness to accept client connection requests, and creates a connection request queue of length backlog to queue incoming connection requests. Once full, additional connection requests are rejected.
所以listen應該是創造一個queue,使之後連進來的socket存放在裡面,創造完就結束了,並不會被block住。
作為客戶端,在創建socket後就調用connect()向服務端發出連接請求。
```
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
```
sockfd
客戶端的socket描述符。
addr
要連接的服務器的socket地址。
addrlen
服務器socket地址的長度。
### accept()
TCP服務器端依次調用socket()、bind()、listen()之後,就會監聽指定的socket地址了。 TCP客戶端依次調用socket()、connect()之後就想TCP服務器發送了一個連接請求。 TCP服務器監聽到這個請求之後,就會調用accept()函數取接收請求,這樣連接就建立好了。之後就可以開始網絡I/O操作了,類同於普通文件的讀寫I/O操作。
```
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
```
sockfd
服務器的socket描述符。
addr
用於返回客戶端的協議地址。
addrlen
協議地址的長度。
注意:accept的第一個參數為服務器的socket描述字,是服務器開始調用socket()函數生成的,稱為監聽socket描述字;而accept函數返回的是已連接的socket描述字。一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命週期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個已連接socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。
If no pending connections are present on the queue, and the socket is not marked as nonblocking, accept() blocks the caller until a connection is present. If the socket is marked nonblocking and no pending connections are present on the queue, accept() fails with the error EAGAIN or EWOULDBLOCK.
所以沒有東西在queue裡時,accept()應該是會hang在那邊。
參考資料:
https://man7.org/linux/man-pages/man2/accept.2.html
例子:
https://www.jianshu.com/p/3b233facd6bb
資料傳輸:
每個 socket 被創建後,都會分配兩個緩衝區,輸入緩衝區和輸出緩衝區。
write ()/send () 並不立即向網絡中傳輸數據,而是先將數據寫入緩衝區中,再由 TCP 協議將數據從緩衝區發送到目標機器。一旦將數據寫入到緩衝區,函數就可以成功返回,不管它們有沒有到達目標機器,也不管它們何時被發送到網絡,這些都是 TCP 協議負責的事情。
TCP 協議獨立於 write ()/send () 函數,數據有可能剛被寫入緩衝區就發送到網絡,也可能在緩衝區中不斷積壓,多次寫入的數據被一次性發送到網絡,這取決於當時的網絡情況、當前線程是否空閒等諸多因素,不由程序員控制。
read ()/recv () 函數也是如此,也從輸入緩衝區中讀取數據,而不是直接從網絡中讀取,如下圖所示

這些 I/O 緩衝區特性如下:
・I/O 緩衝區在每個 TCP 套接字中單獨存在;
・I/O 緩衝區在創建套接字時自動生成;
・即使關閉套接字也會繼續傳送輸出緩衝區中遺留的數據;
・關閉套接字將丟失輸入緩衝區中的數據。
參考資料:
https://www.wangan.com/p/7fygf3e37f493848
參考資料:
https://www.jianshu.com/p/3b233facd6bb
https://www.wangan.com/p/7fygf3e37f493848
https://man7.org/linux/man-pages/man2/accept.2.html
https://www.kshuang.xyz/doku.php/programming:c:socket
# Select()
在前面提到的多進程和多線程來實現服務端與客戶端通信中,不管是多進程還是多線程,一般來說,每一個連接成功的文件描述符(socket)都需要一個進程/線程來進行監控,然後就需要不斷的對每個進程/線程中的文件描述符進行輪詢,例如accept、read等函數,如果沒有連接請求、緩衝區中沒有數據,它們就會一直阻塞住。這樣一來一回就在反覆切換進程/線程,系統的開銷是非常大的。這兩種併發模型都需要進程或者線程自己去等待。
還有一種就是I/O多路複用模型,select就是其中一種。之前多進程多線程是讓進程和線程去阻塞等待,而現在則是直接讓內核去等待,只需要主進程去直接詢問內核哪些描述符準備好了即可。select模型的關鍵就是事先將感興趣的文件描述符放到一個集合中,當用戶進程調用了select,那麼整個進程會被阻塞,而同時,內核就會“監視”所有select感興趣的文件描述符,當任何一個文件描述符中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從內核拷貝到用戶進程。
## Select()缺點
1、內核中對select模型可監視的fd數量限制爲1024,如果要修改這個數字就必須對內核重新編譯;
2、 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低:當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字註冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。
3、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大。
```
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
```
maxfdp:被監聽的文件描述符的最大值大1,因爲文件描述符是從0開始計數的。
readfds、writefds、exceptset:分別指向可讀、可寫和異常等事件對應的描述符集合。
timeout:用於設置select函數的超時時間,即告訴內核select等待多長時間之後就放棄等待。timeout == NULL 表示等待無限長的時間。
timeval結構體定義如下:
```
struct timeval
{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
};
```
return:
超時返回0;失敗返回-1;成功返回大於0的整數,這個整數表示就緒描述符的數目。
### fd_set
```
int FD_ZERO(int fd, fd_set *fdset); //一個 fd_set類型變量的所有位都設爲 0 (可理解爲清空集合中的所有感興趣描述符)
int FD_CLR(int fd, fd_set *fdset); //清除某個位時可以使用 (可理解爲清空集合中的某一個描述符)
int FD_SET(int fd, fd_set *fd_set); //設置變量的某個位置位 (可理解爲向集合中添加一個描述符)
int FD_ISSET(int fd, fd_set *fdset); //測試某個位是否被置位 (可理解爲判斷一個描述符是否在集合中)
```
參考資料:
https://www.twblogs.net/a/5c8abbfebd9eee35fc14b905
WARNING: select() can monitor only file descriptors numbers that are less than FD_SETSIZE (1024)—an unreasonably low limit for many modern applications—and this limitation will not change.All modern applications should instead use poll(2) or epoll(7), which do not suffer this limitation.
select() allows a program to monitor multiple file descriptors,waiting until one or more of the file descriptors become "ready" for some class of I/O operation (e.g., input possible). A file descriptor is considered ready if it is possible to perform a corresponding I/O operation (e.g., read(2), or a sufficiently small write(2)) without blocking.
Note well: Upon return, each of the file descriptor sets is modified in place to indicate which file descriptors are currently "ready". Thus, if using select() with in a loop, thesets must be reinitialized before each call.
所以在每一次while,select之前都要重新指定fd_set的值,而操作完select後,fd_set會被改變,只留下準備好的fd,意思是如果是read_set,沒有資料傳進來,那就不會在set裡面。
參考資料:
https://man7.org/linux/man-pages/man2/select.2.html
### 關於struct timeval
如果將時間的初始化放在外邊,時間初始化為2秒,假設在1秒後發上了事件,則select將會返回並將tv的時間變成上次阻塞的剩餘時間,即1秒,然後再進行監視套接字。這是因為linux系統對select()的實現中會修改參數tv為剩餘時間。所以在循環內部使用函數select的時候一定要在循環內部初始化時間參數。
參考資料:
https://blog.csdn.net/Junkie0901/article/details/14168669
### 問題解決
write資料錯誤或無法使用write:
可能是buffer已滿,換個function或傳慢一點。
每次使用echo前記得先清空buf,否則可能讀到奇怪的東西。(memset)
使用kill -9 強制刪除process
# IPC
### wait
```
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
```
Description
All of these system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed. A state change is considered to be: the child terminated; the child was stopped by a signal; or the child was resumed by a signal. In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then the terminated child remains in a "zombie" state (see NOTES below).
wait(&status) is equivalent to: waitpid(-1, &status, 0);
The value of pid can be:
< -1
meaning wait for any child process whose process group ID is equal to the absolute value of pid.
-1
meaning wait for any child process.
0
meaning wait for any child process whose process group ID is equal to that of the calling process.
.>0
meaning wait for the child whose process ID is equal to the value of pid.
return:
wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.
https://linux.die.net/man/2/wait
### mknod
The system call mknod() creates a filesystem node (file, device special file, or named pipe) named pathname, with attributes specified by mode and dev.
例子:
https://blog.51cto.com/mnt3918290/1828235
## Semaphore
步驟
第1步 - 創建一個信號量或連接到一個已經存在的信號量(semget())
第2步 - 對信號量執行操作,即分配或釋放或等待資源(semop())
第3步 - 在消息隊列(semctl())上執行控制操作
### semget
```
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg)
```
第一個參數key用於識別消息隊列。key可以是任意值,也可以是來自庫函數ftok()的值。
第二個參數nsems指定了信號的數量。 如果二進制那麼它是1,意味着需要1個信號集,否則按照所需的信號量集計數。
第三個參數semflg指定所需的信號量標誌,如IPC_CREAT(如果不存在則創建信號量)或IPC_EXCL(與IPC_CREAT一起用於創建信號量,如果信號量已經存在,則調用失敗)。 還需要傳遞權限。
需要key是因為我們需要在不同的process間使用相同的semaphore,這時給他們相同的key就能創造出相同的key(應該啦)。
參考資料:
https://www.1ju.org/ipc/semaphores
### semop
```
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
```
semop() performs operations on selected semaphores in the set indicated by semid. Each of the nsops elements in the array pointed to by sops is a structure that specifies an operation to be performed on a single semaphore. The elements of this
structure are of type struct sembuf, containing the following members:
```
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
```
Flags recognized in sem_flg are IPC_NOWAIT and SEM_UNDO. If an operation specifies SEM_UNDO, it will be automatically undone when the process terminates.
The set of operations contained in sops is performed in array order, and atomically, that is, the operations are performed either as a complete unit, or not at all. The behavior of the system call if not all operations can be performed immediately depends on the presence of the IPC_NOWAIT flag in the individual sem_flg fields, as noted below.
參考資料:
https://man7.org/linux/man-pages/man2/semop.2.html
https://blog.csdn.net/wanzyao/article/details/55271103
sem_flg:
IPC_NOWAIT
Will cause semop() to return EAGAIN rather than place the thread into wait state.
SEM_UNDO
Will result in semadj adjustment values being maintained for each semaphore on a per process basis. If sem_op value is not equal to zero and SEM_UNDO is specified, then sem_op value is subtracted from the current process's semadj value for that semaphore. When the current process is terminated, see exit(), the semadj value(s) will be added to the semval for each semaphore. The semctl() command SETALL may be used to clear all semadj values in all processes. If __IPC_BINSEM was specified on semget for this semaphore, the Sem_UNDO flag will cause an error to be returned.
參考資料:
https://www.ibm.com/docs/en/zos/2.4.0?topic=functions-semop-semaphore-operations
### semctl()
The semctl() function performs control operations in semaphore set semid as specified by the argument cmd.
https://www.ibm.com/docs/en/zos/2.2.0?topic=functions-semctl-semaphore-control-operations
https://man7.org/linux/man-pages/man2/semctl.2.html
IPC_RMID:
Immediately remove the semaphore set, awakening all processes blocked in semop(2) calls on the set (with an error return and errno set to EIDRM). The effective user ID of the calling process must match the creator or owner of the semaphore set, or the caller must be privileged. The argument semnum is ignored.
GETVAL:
Return semval (i.e., the semaphore value) for the semnum-th semaphore of the set. The calling process must have read permission on the semaphore set.
SETVAL:
Set the semaphore value (semval) to arg.val for the semnum-th semaphore of the set, updating also the sem_ctime member of the semid_ds structure associated with the set. Undo entries are cleared for altered semaphores in all processes. If the changes to semaphore values would permit blocked semop(2) calls in other processes to proceed, then those processes are woken up. The calling process must have alter permission on the semaphore set.
### shared memory:
https://www.delftstack.com/zh-tw/howto/c/shmget-example-in-c/