本次内容将介绍TCP客户/服务器模型,回射客户/服务器,以及一系列函数:socket, bind, listen, accept, connect等。
1.TCP客户/服务器模型(C/S模型)
(1)模型如下:
2.回射客户/服务器
(1)模型如下
3.相关函数
(1)socket函数
- 包含头文件<sys/socket.h>
- 功能:创建一个套接字用于通信
- 原型:int socket(int domain, int type, int protocol);
- 参数:
- domain:指定通信协议族(protocol family)
- type:指定socket类型, 流式套接字SOCK_STREAM, 数据报套接字SOCK_DGRAM, 原始套接字SOCK_RAW
- protocol:协议类型
- 返回值:成功返回非负整数,它与文件描述符类似,我们把它称为套接口描述字,简称套接字。失败返回-1。
(2)bind函数
- 包含头文件<sys/socket.h>
- 功能:绑定一个本地地址到套接字
- 原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数:
- sockfd:socket函数返回的套接字
- addr:要绑定的地址
- addrlen:地址长度
- 返回值:成功返回0;失败返回-1
(3)listen函数
- 包含头文件<sys/socket.h>
- 功能:将套接字用于监听进入的连接
- 原型:int listen(int sockfd, int backlog);
- 参数:
- sockfd:socket函数返回的套接字
- backlog:规定内核为此套接字排队的最大连接个数,一般使用SOMAXCONN宏
- 返回值:成功返回0;失败返回-1
1)一旦调用listen函数之后,该套接字就变成了被动套接字,否则默认是主动套接字:
被动套接字:接受连接——当有连接时调用accept(服务器端)
主动套接字:发起连接——需要调用connect(客户端)
2)一般来说,listen函数应该在调用socket和bind函数之后,调用函数accept之前调用。(服务器端)
3)对于给定的监听套接口,内核要维护两个队列:
a)已由客户发出并达到服务器,服务器正在等待完成TCP三路握手的队列
b)已完成连接队列
4)针对上述过程,有如下示意图:
其中accept之后,就会将其条目从已完成队列中移除,以便有更多的客户端能够发起连接。
(4)accept函数
- 包含头文件<sys/socket.h>
- 功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
- 原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 参数:
- sockfd:服务器套接字
- addr:将返回对等方的套接字地址
- addrlen:返回对等方的套接字地址长度
- 返回值:成功返回非负整数,失败返回-1
(5)connect函数
- 包含头文件<sys/socket.h>
- 功能:建立一个连接至addr所指定的套接字
- 原型:int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
- 参数:
- sockfd:未连接的套接字
- addr:要连接套接字地址
- addrlen:第二个参数addr长度
- 返回值:成功返回0;失败返回-1
4.回射客户服务器实例
(1)服务器端,echosrv.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#include <stdio.h> #include <stdlib.h> #include <unistd.h>//read write在这个头文件里 #include <errno.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0) int main(void) { int listenfd; if((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htonl(512119); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind"); if (listen(listenfd, SOMAXCONN) < 0) ERR_EXIT("listen"); struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int conn; if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0) ERR_EXIT("accept"); /*连接建立之后,打印出客户端的ip和port*/ printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); char recvbuf[1024]; while(1) { memset(recvbuf, 0, sizeof(recvbuf)); int ret = read(conn, recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout); write(conn, recvbuf, ret); } return 0; } |
(2)客户端,echocli.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0) int main(void) { int sock; if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htonl(512119); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("connect"); char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) { write(sock, sendbuf, strlen(sendbuf)); read(sock, recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout); } close(sock); return 0; |
编译之后,先运行服务器再运行客户端,在客户端输入消息可得到如下输出:
1 2 3 4 5 6 7 8 |
//服务器端 [root@VM_198_209_centos 07_demon]# ./echosrv hello world! //客户端 [root@VM_198_209_centos 07_demon]# ./echocli hello world! hello world! |
建立连接之后,当从客户端输入“hello world!”,服务器端会收到来自客户端发送的消息,之后服务器端再将此消息回射给客户端,客户端打印输出。
附:
(1)memset函数
- 包含头文件<string.h>
- 函数原型:void* memset(void *s, int ch, size_t n);
- 函数功能:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s。作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法。
WARNING:该函数对数组操作时只能用于数组的置0或-1,其他值无效。
(2)fgets函数和fputs函数
char * fgets( char * buf, int n, FILE * fp ); // 从fp读取字符串到缓冲区buf中
int fputs( const char * str, FILE * fp ); // 将缓冲区str中的数据写入fp中
1)fgets中buf为存储字符串的地址,n为读取字符串的长度,fp为文件的指针,返回值为空指针时表示获取失败或结束。该函数一次最多只读取一行,遇到\n就会停止读取,若有多行需要循环读取。
2)fputs中str为要写入文件的字符串,fp为要操作的文件,返回值为0表示成功。写入的字符串也是以结束符\n为结束的,所以多行写入需要重复操作。
《Linux网络编程之socket编程(二)》有1个想法