Linux C简单服务器模型解析及完整代码
- 1.服务器端流程及解析
- 2.服务器端代码
- 3.客户端流程及解析
- 4.客户端代码
- 5.不足之处
(注:流程解析可结合相应代码处一起理解)
1.服务器端流程及解析
服务器端做的事情:
1.创建监听的套接字。
int socket(int domain, int type, int protocol)
- domain:协议族,可用AF_INET(代表IPv4),AF_INET6(代表IPv6),还有本地的通信AF_LOCAL(进程间通信),常用AF_INET。
- type:使用的协议类型,是流式协议还是报式协议。对应的参数为SOCK_STREAM或SOCK_DGRAM。
- protocol:使用的具体协议。一般填0,如果是流式协议默认使用TCP,如果是报式协议默认使用UDP。
- 返回:创建成功则返回套接字的文件描述符。创建失败返回-1.
2.绑定。将监听所用的套接字绑定相关IP端口信息。
这里存储IP和端口的结构体有专用地址和通用地址,在我们自己构建的时候常常使用的是专用地址,而在具体使用的时候都要转化为通用地址。比如这里的专用地址有sockaddr_in (代表IPv4),sockaddr_in6(代表IPv6),sockaddr_un(Unix本地协议族)。然后通用地址为sockaddr。然后我们非常常用的就是一个sockaddr_in结构体,以IPv4为例。然后有不懂的结构体可以直接在Linux系统中查看man文档(比如man 2 socket, man 2 bind),并且有这些专门的结构体定义及所用的头文件。
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON (sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr;
};
对于sockaddr_in来说,也就是IPv4地址,要记录三部分内容,第一个是协议族,常用的也就是前面提到的AF_INET,第二部分为接受的IP地址,当选用INADDR_ANY时意味着接受任意的IP地址,第三个为使用的端口,htons代表着host to net short,也就是unsigned short的主机字节序到网络字节序的转换,两个字节的转换,端口号恰好可以用两个字节16位的大小来存储。同理ntohl代表的是net to host long,也就是long类型的数据从网络字节序转换到主机字节序,至于long类型,恰好对应的是IP地址32位,四个字节。至于上面的第四个参数,是用于结构体内存对齐的,就不用管了。
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- sockfd:需要绑定的套接字。这里也就是第一步创建的套接字。
- addr:注意看它的类型,这里是通用地址指针,而我们之前使用的都是sockaddr_in(IPv4专用地址),故这里的话需要做一个类型转换,直接类型强转即可。
- addrlen:代表的是地址长度。
- 返回:如果绑定成功返回0。如果绑定失败则发挥-1,并且会设置错误号。
3.监听。这里的绑定是监听所绑定的套接字。
int listen(int sockfd, int backlog);
- sockfd:监听套接字,这里也就是前面初始套接字与IP端口绑定好的有完整内容的套接字。
- backlog:设置最大连接数。一般设置为5即可,因为很多时候使用了服务器资源又会马上释放了。
- 返回:如果绑定成功返回0。如果绑定失败则发挥-1,并且会设置错误号。
4.接受客户端连接。接受连接所用的函数是accept,如果没有客户端连接进入,那么就不会执行下面的代码,也就是不会执行下面的具体通信,是阻塞在那的。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:放入被监听的套接字。
- addr:放入一个客户端信息结构体,主要存放的是IP端口这些信息,这里同样可以用sockaddr_in结构体,只需转换一下即可,注意这里可用一个传参,之后如果想使用客户端的信息的时候就非常方便。
- addrlen:客户端结构体的大小,直接sizeof即可。
- 返回:接受客户端连接成功返回一个新的套接字(文件描述符),接受连接失败返回-1并且设置错误号。
5.通信(通常情况:先recv后send)
服务器端通常的一个通信流程通常都是先接收到客户端请求的信息,然后再发送对应的消息给客户端。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd:通信所用的套接字,是accept接受后产生的套接字,而不是监听的套接字,注意区分。
- buf:接收数据所用的缓冲区,这里可以用字符数组,然后做一个强制类型转换。
- len:代表的是接收到的数据长度。
- flags:一般不用管。
- 返回:为0,对方断开连接。为>0,则代表接收到了数据,可进一步处理。为-1,代表出错,并且设置错误号。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd:通信所用的套接字,是accept接受后产生的套接字,而不是监听的套接字,注意区分。
- buf:需要发送给客户端的数据缓冲区,和前面的接收缓冲区相区分,同样也可做一个类型转换。char *转换为void *。
- len:发送出去的数据长度。
- flags:一般不用管。
6.释放资源
主要是监听所用的套接字和连接所用的套接字。
2.服务器端代码
//创建TCP服务器端
//时间:2022年3月26日15:30:18
//作者:credic_1017
//sever.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(){
//1.创建socket(用于监听的套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if ( lfd == -1 ){
perror("socket");
exit(-1);
}
//2.绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
//inet_pton(AF_INET, "192.168.204.128",saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if ( ret == -1 ){
perror("bind");
exit(-1);
}
//3.监听
ret = listen(lfd, 8);
if ( ret == -1 ){
perror("listen");
exit(-1);
}
//4.接收客户端连接
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if( cfd == -1 ){
perror("accept");
exit(-1);
}
//输出客户端信息
char clientIP[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is: %s, port is %d\n", clientIP, clientPort);
//5.获取客户端信息
char recvBuf[1024] = {0};
char data[1024] = {0};
while(1){
int recvlen = read(cfd, recvBuf, sizeof(recvBuf));
sleep(1);
if( recvlen == -1 ){
perror("read");
exit(-1);
}else if(recvlen > 0){
printf("rece client data: %s\n", recvBuf);
}else if(recvlen == 0){
//表示客户端断开连接
printf("client closed\n");
}
//给客户端发送数据
fgets(data, 1024 , stdin);
data[1023] = '\0';
write(cfd, data, strlen(data));
}
//关闭文件描述符
close(lfd);
close(cfd);
return 0;
}
3.客户端流程及解析
1.创建套接字
2.连接服务器端
首先创建服务器套接字,然后填写服务器IP端口,注意IP和端口都得做一个本地字节序到网络字节序的转换。这里端口进行字节序转换使用的函数是htons,而IP使用的是inet_pton,主要是由于IP地址常用点分十进制的记法,而端口则直接是一个整数,如果是一个大整数则可以用htonl函数,具体含义在前面的服务器端解析已经说过了。
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- sockfd:连接所用的套接字。
- addr:需要进行连接的服务器端存储结构体。
- addrlen:服务器信息存储结构体大小。
- 返回:连接成功,返回0。连接失败返回-1并且设置错误号。
3.通信(通常先send再recv)
作为客户端通常先发送消息给服务器端,之后再接收来自服务器端的消息。具体流程和上面服务器端通信类似。
4.断开连接释放资源
通常而言是客户端断开连接的,当然服务器端先断开连接也是可以的。释放资源主要是释放套接字(文件描述符)。
4.客户端代码
//创建TCP客户端
//时间:2022年3月27日15:13:07
//作者:credic_1017
//client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(){
//1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
//2.连接服务器端
struct sockaddr_in severaddr;
severaddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.204.128", &severaddr.sin_addr.s_addr);
severaddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&severaddr, sizeof(severaddr));
if (ret == -1){
perror("connect");
exit(-1);
}
//3.通信
char recvBuf[1024] = {0};
char data[1024] = {0};
while(1){
fgets(data, 1024 , stdin);
data[1023] = '\0';
write(fd, data, strlen(data));
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if( len == -1 ){
perror("read");
exit(-1);
}else if(len > 0){
printf("rece sever data: %s\n", recvBuf);
}else if(len == 0){
//表示客户端断开连接
printf("sever closed...\n");
}
}
close(fd);
return 0;
}
5.不足之处
目前的通信是一对一的,单个进程对单个进程,也就是上面的通信服务器端也面对两个及两个以上客户端的连接是无法进行处理的。在两个连接的情况下,一个客户端资源已经释放,第二个客户端依旧不能与服务器端进行通信。这是一个很大的弊端,还在学习多线程及多进程进行服务器端进行高并发,能够较好的解决这个问题。