C語言多線程服務器的實現實例
本文基于 C 標準庫提供的網絡通信 API,使用 TCP ,實現一個簡單的多線程服務器 Demo 。
首先要看 API
API
字節(jié)序轉換
函數原型:
#include <arpa/inet.h> uint64_t htonll(uint64_t hostlonglong); uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint64_t ntohll(uint64_t netlonglong); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
h
表示 host, n
表示 network,這些函數的作用是把主機的字節(jié)序轉換為網絡的字節(jié)序(即小端到大端的轉變)。
例如:
#include <arpa/inet.h> #include <stdio.h> int main() { uint32_t host = 0x01020304; // high->low: 01 02 03 04 uint32_t network = htonl(host); // high->low: 04 03 02 01 printf("%p\n", network); // 0x4030201 }
socket
函數原型:
#include <sys/socket.h> int socket(int domain, int type, int protocol);
建立一個協議族為 domain
, 協議類型為 type
, 協議編號為 protocol
的套接字文件描述符。如果函數調用成功,會返回一個標識這個套接字的文件描述符,失敗的時候返回-1。
domain
的取值:
Name Purpose Man page AF_UNIX, AF_LOCAL Local communication unix(7) AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) AF_IPX IPX - Novell protocols AF_NETLINK Kernel user interface device netlink(7) AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7) AF_AX25 Amateur radio AX.25 protocol AF_ATMPVC Access to raw ATM PVCs AF_APPLETALK AppleTalk ddp(7) AF_PACKET Low level packet interface packet(7) AF_ALG Interface to kernel crypto API
AF
是 Address Family 的縮寫,INET
是 Internet 的縮寫。某些地方可能會使用 PF
,即 Protocol Family,應該是同一個東西。
type
的取值:
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported. SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length). SOCK_SEQPACKET Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with each input system call. SOCK_RAW Provides raw network protocol access. SOCK_RDM Provides a reliable datagram layer that does not guarantee ordering. SOCK_PACKET Obsolete and should not be used in new programs; see packet(7).
type
常用的是 STREAM
和 DGRAM
,根據描述,可以確定前者對應 TCP,而后者對應 UDP :
SOCK_STREAM
套接字表示一個雙向的字節(jié)流,與管道類似。流式的套接字在進行數據收發(fā)之前必須已經連接,連接使用connect()
函數進行。一旦連接,可以使用read()
或者write()
函數進行數據的傳輸,流式通信方式保證數據不會丟失或者重復接收。SOCK_DGRAM
和SOCK_RAW
這個兩種套接字可以使用函數sendto()
來發(fā)送數據,使用recvfrom()
函數接受數據,recvfrom()
接受來自制定IP地址的發(fā)送方的數據。
對于第 3 個參數 protocal
,用于指定某個協議的特定類型,即 type
類型中的某個類型。通常某協議中只有一種特定類型,這 樣protocol
參數僅能設置為 0 ;但是有些協議有多種特定的類型,就需要設置這個參數來選擇特定的類型。
bind
函數原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
如果函數執(zhí)行成功,返回值為 0,否則為 SOCKET_ERROR
。
參數:
sockfd
是一個有效的 socket 描述符(函數socket()
的有效返回值)。addrlen
是第二個參數addr
結構體的長度。addr
是一個sockaddr
結構體指針,包含 IP 和端口等信息。
sockaddr
的結構如下:
struct sockaddr { sa_family_t sa_family; char sa_data[14]; }; // sa_familt_t 是無符號整型,Ubuntu 下是 unsigned short int
sockaddr
的存在是為了統一地址結構的表示方法 ,統一接口函數,使得不同的地址結構可以被 bind(), connect(), recvfrom(), sendto()
等函數調用。但一般的編程中并不直接對此數據結構進行操作,而使用另一個與之等價的數據結構 sockaddr_in
:
struct sockaddr_in { short int sin_family; /* Address family */ unsigned short int sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ unsigned char sin_zero[8]; /* Same size as struct sockaddr */ };
各字段解析:
sin_family
:指代協議族,在 socket 編程中有 3 個取值AF_INET, AF_INET6, AF_UNSPEC
.sin_port
:存儲端口號(使用網絡字節(jié)順序)sin_addr
:存儲IP地址,使用in_addr
這個數據結構sin_zero
:是為了讓sockaddr
與sockaddr_in
兩個數據結構保持大小相同而保留的空字節(jié)。
in_addr
的結構如下:
typedef uint32_t in_addr_t; struct in_addr{ in_addr_t s_addr; };
listen
int listen(int sockfd, int backlog);
返回值:無錯誤,返回 0,否則 -1 。
作用:listen
函數使用主動連接套接字變?yōu)楸贿B接套接口,使得一個進程可以接受其它進程的請求,從而成為一個服務器進程。在 TCP 服務器編程中 listen
函數把進程變?yōu)橐粋€服務器,并指定相應的套接字變?yōu)楸粍舆B接。
listen
函數一般在調用 bind
之后,調用 accept
之前調用。
backlog
參數指定連接請求隊列的最大個數。
accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
接受連接請求,成功返回一個新的套接字描述符 newfd
,失敗返回-1。返回值 newfd
與參數 sockfd
是不同的,newfd
專門用于與客戶端的通信,而 sockfd
是專門用于 listen
的 socket 。
addr
和 addrlen
都是指針,用于接收來自客戶端的 addr
的信息。
inet_addr
函數原型:
in_addr_t inet_addr(const char *cp);
將一個點分十進制的 IP 字符串轉換為網絡字節(jié)序的 uint32_t
。
例子
int main() { const char *ip = "127.0.0.1"; // 7f.00.00.01 printf("%p\n", inet_addr(ip)); // 0x0100007f }
send
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const 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 sendmsg(int sockfd, const struct msghdr *msg, int flags);
其中 send(fd, buf, len, flags)
與 sendto(fd, buf, len, flags, NULL, 0)
等價。
recv
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
其中 recv(fd, buf, len, flags)
與 recvfrom(fd, buf, len, flags, NULL, 0)
等價。
connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
成功返回 0 ,失敗返回 -1 。
sockfd
是客戶端進程創(chuàng)建的,用于與服務端通信的 socket ; addr
是目標服務器的 IP 地址和端口。
多線程服務器
本次實現的場景如下:
- 客戶端可以具有多個,客戶端主動連接服務器,允許每個客戶端發(fā)送
msg
到服務器,并接受來自服務器的信息。 - 服務端對于每個申請連接到客戶端,創(chuàng)建一個線程處理請求。對于客戶端發(fā)送過來的
msg
,然后服務器把msg
加上一些其他字符串,發(fā)送回客戶端。
server
#include <stdlib.h> #include <stdio.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <pthread.h> #include <string.h> #include <unistd.h> #define PORT 8887 #define QUEUE 10 const char *pattern = "Hello, I am the server. Your msg is received, which is: %s"; typedef struct { struct sockaddr_in addr; socklen_t addr_len; int connectfd; } thread_args; void *handle_thread(void *arg) { thread_args *targs = (thread_args *)arg; pthread_t tid = pthread_self(); printf("tid = %u and socket = %d\n", tid, targs->connectfd); char send_buf[BUFSIZ] = {0}, recv_buf[BUFSIZ] = {0}; while (1) { int len = recv(targs->connectfd, recv_buf, BUFSIZ, 0); printf("[Client %d] %s", targs->connectfd, recv_buf); if (strcmp("q\n", recv_buf) == 0) break; sprintf(send_buf, pattern, recv_buf); send(targs->connectfd, send_buf, strlen(send_buf), 0); memset(send_buf, 0, BUFSIZ), memset(recv_buf, 0, BUFSIZ); } close(targs->connectfd); free(targs); pthread_exit(NULL); } int main() { int listenfd = socket(AF_INET, SOCK_STREAM, 0); printf("server is listening at socket fd = %d\n", listenfd); struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listenfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("bind error\n"); exit(-1); } if (listen(listenfd, QUEUE) == -1) { perror("listen error\n"); exit(-1); } while (1) { thread_args *targs = malloc(sizeof(thread_args)); targs->connectfd = accept(listenfd, (struct sockaddr *)&targs->addr, &targs->addr_len); // int newfd = accept(sockfd, NULL, NULL); pthread_t tid; pthread_create(&tid, NULL, handle_thread, (void *)targs); pthread_detach(tid); } close(listenfd); }
client
#include <stdio.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define PORT 8887 const char *target_ip = "127.0.0.1"; int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); printf("client socket = %d\n", sockfd); struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = inet_addr(target_ip); if (connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0) { perror("connect error\n"); exit(-1); } char send_buf[BUFSIZ], recv_buf[BUFSIZ]; while (fgets(send_buf, BUFSIZ, stdin) != NULL) { if (strcmp(send_buf, "q\n") == 0) break; send(sockfd, send_buf, strlen(send_buf), 0); printf("[Client] %s\n", send_buf); recv(sockfd, recv_buf, BUFSIZ, 0); printf("[Server] %s\n", recv_buf); memset(send_buf, 0, BUFSIZ), memset(recv_buf, 0, BUFSIZ); } close(sockfd); exit(0); }
運行結果
編譯:
gcc server.c -o server -lpthread
gcc client.c -o client
先運行 server
,后運行多個 client
.
需要注意的是,這里的服務器,客戶端都是運行在同一機器上的,所以客戶端使用的目標 IP 是 127.0.0.1 ,如果想進一步更全面地測試,應該把服務端運行在一個云服務器上,然后開放 8887 端口,再進行測試。
到此這篇關于C語言多線程服務器的實現實例的文章就介紹到這了,更多相關C語言多線程服務器的實現內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
探討:C++實現鏈式二叉樹(用非遞歸方式先序,中序,后序遍歷二叉樹)
本篇文章是對用C++實現鏈式二叉樹(用非遞歸方式先序,中序,后序遍歷二叉樹)的方法進行了詳細的分析介紹,需要的朋友參考下2013-05-05