這里在Ubuntu環境下演示
一般流程
服務端常用函數:
socket()
:創建一個新的套接字。bind()
:將套接字與特定的IP地址和端口綁定。listen()
:使套接字開始監聽傳入的連接請求。accept()
:接受一個傳入的連接請求,并創建一個新的套接字用于與客戶端通信。send()
?或?write()
:向連接的客戶端發送數據。recv()
?或?read()
:接收來自客戶端的數據。close()
:關閉與客戶端通信的套接字。
流程通常是:
socket() -> bind() -> listen() -> accept() -> send()或write() / recv()或read() -> close()
客戶端常用函數:
socket()
:創建一個新的套接字。connect()
:嘗試與服務端建立連接。send()
?或?write()
:向服務端發送數據。recv()
?或?read()
:接收來自服務端的數據。close()
:關閉與服務端通信的套接字。
流程通常是:
socket() -> connect -> send()或write() / recv()或read() -> close()
簡單函數介紹
socket()
socket用于創建一個套接字。
所謂套接字(Socket),就是對網絡中不同主機上的應用進程之間進行雙向通信的端點的抽象。一個套接字就是網絡上進程通信的一端,提供了應用層進程利用網絡協議交換數據的機制。從所處的地位來講,套接字上聯應用進程,下聯網絡協議棧,是應用程序通過網絡協議進行通信的接口,是應用程序與網絡協議棧進行交互的接口
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ????????????????????????????????——百度百科
Linux中socket的返回類型是整形,Windows中返回的是SOCKET類型。
int socket(int af,int type,int protocol);//windows中的返回類型是SOCKET
socket的三個參數:
? ?//例如一次初始化:
? ?int s = socket(AF_INET, SOCK_STREAM, 0);
? ?
? ? //三個參數:
? ? /*
? ? ? ? (1)地址族:
? ? ?* AF_INET表示使用IPv4(Internet Protocol version 4)地址。
? ? ?* 它指定了套接字將在IPv4網絡上使用。
? ? ?* 與之對應的是AF_INET6,用于IPv6地址。
? ? ? ? (2)套接字類型(Socket Type):
? ? ?* SOCK_STREAM表示套接字是一個面向連接的、可靠的、基于字節流的套接字,通常用于TCP(Transmission Control Protocol)連接。
? ? ?* 與之對應的是SOCK_DGRAM,表示一個無連接的、固定最大長度消息、不可靠的套接字,通常用于UDP(User Datagram Protocol)連接。
? ? ? ? (3)協議:
? ? ?* 這個參數通常設置為0,表示讓系統自動選擇適合指定地址族和套接字類型的協議。
? ? ?* 在大多數情況下,對于AF_INET和SOCK_STREAM的組合,系統會選擇TCP協議。
? ? ?* 同樣地,對于AF_INET和SOCK_DGRAM的組合,系統會選擇UDP協議。
? ? ?*/
connect()
connect
函數用于建立客戶端和服務器之間的連接。
它屬于套接字編程接口,用于將一個套接字與遠程服務器的特定套接字地址關聯起來,從而初始化一個連接。
connect函數原型:
int connect(SOCKET s,const struct sockaddr *name,int namelen);
其中,struct sockaddr的類型:
struct sockaddr { sa_family_t sin_family;//地址族char sa_data[14]; };
這個類型需要獲取到服務器的IP與端口,但是不好進行操作,所以我們要用到跟其作用一致的結構體——struct sockaddr_in:
struct sockaddr_in
{sa_family_t sin_family;//地址族uint16_t sin_port; //端口struct in_addr sin_addr; //32位IPchar sin_zero[8];
}
? ?//例如,對struct sockaddr_in進行一次初始化
????struct sockaddr_in server_addr;
? ? //給該結構體清空
? ? memset(&server_addr, 0, sizeof(server_addr));
? ? //初始化,要拿到主機的ip和端口
? ? server_addr.sin_family = AF_INET;
? ? server_addr.sin_port = htons(port);
? ? server_addr.sin_addr.s_addr = inet_addr(ip);? ?
那么connect傳參類似如下:
if (connect(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
? ? ? ? perror("Connect failed");
? ? ? ? // 連接失敗
? ? }
send()
send()
?是一個用于通過套接字(socket)發送數據的核心函數
send()函數原型:
int send(SOCKET s,const char *buf,int len,int flags);
其中
?//——最后一個參數flags用于控制信息發送方式,這里0是默認發送方式
?//send返回的是實際發送的字節數,失敗則返回-1
簡單客戶端實現代碼
客戶端實現代碼:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int tcp_echo_client_start(const char *ip, int port)
{printf("tcp echo client, ip: %s, port:%d\n", ip, port);int s = socket(AF_INET, SOCK_STREAM, 0);//1、創建套接字//如果創建套接字失敗if(s < 0){perror("tcp echo client: open socket error");return -1;}//2、用connect函數向服務器建立連接struct sockaddr_in server_addr;//給該結構體清空memset(&server_addr, 0, sizeof(server_addr));//初始化,要拿到主機的ip和端口server_addr.sin_family = AF_INET;server_addr.sin_port = htons(port);server_addr.sin_addr.s_addr = inet_addr(ip);if (connect(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("Connect failed");// 處理錯誤}//3、從鍵盤中讀取輸入,并發送數據char buf[128];printf("please input:");while(fgets(buf, sizeof(buf), stdin) != NULL){//發送,發送給套接字就是交給操作系統去管理if(send(s, buf, strlen(buf), 0) < 0){perror("write error");close(s);return -1;}//服務端收到消息會響應,需要接受數據memset(buf, 0, sizeof(buf));int len = recv(s, buf, sizeof(buf) - 1, 0);if(len < 0)perror("read error");//接受數據并打印printf("%s\n", buf);printf(">>\n");}//關閉close(s);//Windows中采用closesocket()函數return 0;
}int main()
{tcp_echo_client_start("192.168.74.1", 8080);return 0;
}
服務端實現
bind()
bind()
?函數用于將一個?套接字(socket)?綁定到特定的?IP 地址和端口號,通常用于服務器端設置監聽地址
bind()函數原型:
int bind(int sockfd, // 套接字文件描述符(由 socket() 創建)const struct sockaddr *addr, // 指向 sockaddr 結構體的指針(存儲地址信息)socklen_t addrlen // sockaddr 結構體的長度
);
//成功返回0,失敗返回-1
sockfd:
由?
socket()
?創建的套接字描述符(如?int sockfd = socket(AF_INET, SOCK_STREAM, 0);
)。addr:
指向?
struct sockaddr
?的指針,存儲?IP 地址 + 端口號。實際使用時通常用?
struct sockaddr_in
(IPv4)或?struct sockaddr_in6
(IPv6),并強制轉換為?sockaddr*
。
listen()
listen()
?函數用于將?TCP 套接字?設置為?被動監聽模式,等待客戶端發起連接請求(connect()
)
#include <sys/socket.h>int listen(int sockfd, // 已綁定地址的套接字描述符(由 socket() 創建 + bind() 綁定)int backlog // 允許排隊的最大未完成連接數(直接影響并發處理能力)
);
//成功返回0,失敗返回-1
sockfd:
必須是已通過?
bind()
?綁定到某個?IP + 端口?的?流式套接字(如?SOCK_STREAM
,即 TCP 套接字)。如果未綁定,系統會隨機分配一個端口(但服務器通常需要固定端口,所以必須顯式?
bind()
)。backlog:
定義?已完成三次握手但未被?
accept()
?取走的連接隊列的最大長度(即“等待處理的連接”)。不同操作系統對?
backlog
?的實現有差異,但通常遵循以下規則:
Linux:實際隊列長度 =?
min(backlog, /proc/sys/net/core/somaxconn)
(默認值通常為?128
)。Windows:直接使用?
backlog
,但最大值由系統限制。推薦值:
高并發服務器:
128
?或更高(需調整系統參數?somaxconn
)。簡單測試:
5
~10
。
accept()
accept()
?函數用于?從已監聽的 TCP 套接字中接受一個客戶端的連接請求,并返回一個新的套接字描述符
#include <sys/socket.h>//成功:返回一個新的 套接字描述符(int),專門用于與客戶端通信。
//失敗:返回-1
int accept(int sockfd, // 已調用 listen() 的監聽套接字struct sockaddr *addr, // 用于存儲客戶端地址信息(可選,可設為 NULL)socklen_t *addrlen // 客戶端地址結構體的長度(輸入輸出參數)
);
服務端
在Ubuntu中實現的:
#include <sys/socket.h>
#include <error.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h> // 包含close函數的聲明
#include <pthread.h>
#include <sys/poll.h>void *client_thread(void *arg)
{int clientfd = *(int *)arg;while(1){char buffer[128] = {0};int count = recv(clientfd, buffer, 128, 0); if(count == 0){break;}send(clientfd, buffer, count, 0); // 只發送接收到的數據長度printf("clientfd: %d, count: %d, buffer:%s\n", clientfd, count, buffer); }close(clientfd); //關閉return NULL;
}int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);//綁定IPstruct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(struct sockaddr_in));serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);serveraddr.sin_port = htons(8080);// 使用 `bind` 函數將套接字綁定到指定的地址和端口上。如果綁定失敗,程序將打印錯誤信息并退出。if(-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))){perror("bind");return -1;}listen(sockfd, 10); //使用 `listen` 函數使套接字進入監聽狀態,等待傳入連接,隊列長度設置為 10。fd_set rfds, rset;FD_ZERO(&rfds); //將其清空FD_SET(sockfd, &rfds);int maxfd = sockfd;while(1) {rset = rfds;int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);//timeout參數設置為NULL,就是一直等待if(FD_ISSET(sockfd, &rset)){struct sockaddr_in clientaddr;socklen_t len = sizeof(clientaddr);int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept\n");FD_SET(clientfd, &rfds);if(clientfd > maxfd)maxfd = clientfd;}for(int i = 0; i <= maxfd; i++){if(FD_ISSET(i, &rset) && i != sockfd){//讀char buffer[128] = {0};int count = recv(i, buffer, 128, 0); if(count == 0){printf("disconnect\n");FD_CLR(i, &rfds);close(i);}else{send(i, buffer, count, 0);printf("clientfd: %d, count: %d, buffer:%s\n", i, count, buffer); }}}}getchar(); // 等待用戶輸入以便保持程序運行 return 0;
}
演示效果
先編譯:
運行server端后,可以查看8080端口,查看其狀態
netstat -tulnp | grep 8080
然后在客戶端中連接服務端:
在客戶端這邊使用鍵盤輸入:
客戶端斷開后,服務端提示:
不過服務端依舊在LISTEN: