项目来源:登录—专业IT笔试面试备考平台_牛客网
该课程教授了如何使用socket API实现一个基于TCP协议的多进程并发的服务器。实现服务器之前,首先了解一下TCP通信的流程和所要用的各个API。
1.TCP通信流程
TCP通信中,发起连接请求的一端称为客户端,被动接受连接的一端称为服务端。
服务端:
1.创建一个用于监听的套接字 ????????-监听:监听客户端的连接 ????????-套接字:一个socket文件描述符……socket() 2.将这个监听文件的文件描述符和本地IP和端口绑定(也就是绑定socket地址信息)……bind() ????????-客户端连接服务器的时候使用的就是这个IP和端口 3.设置监听,监听文件描述符开始工作(客户端socket文件给服务端socket文件的读缓冲区发送数据,服务端socket就是监听自己的读缓冲区有无数据,该过程由OS完成)……listen() ? ? ? ? -监听本身不接受请求,发现缓冲区有连接后会转去下一步 4.阻塞等待,直到客户端发起连接,接收客户端连接后会产生另一个用于和客户端通信的socket……accept() 5.通信。……Window:recv()/send() Linux:read()/write() 6.通信结束,关闭连接。……close()
为什么监听到请求后要发起另一个socket用于和客户端通信?
假设有两个客户端先后发起连接而用同一个socket接受连接和进行通信,接受第一个客户端连接后socket开始通信,这个时候第二个客户端也发了一个请求过来,socket这边如果接受请求就得同时跟两个客户端进行通信,而socket文件中只能记下一个客户端的地址信息,这不就乱套了吗。因此必须将接受连接的socket和用于通信的socket区分开来,用一个socket专门接受连接,有多少个连接进来就新建多少个socket与之通信。
这也体现了TCP面向连接的特点,即通信只能是一对一的,一个socket对应一个socket。
?客户端:
1.创建一个用于通信的套接字。(端口号由os自动分配的)……socket() 2.连接服务器,需要制定目的服务器的IP和端口……connect() 3.连接成功,进行通信……send() 4.通信结束,断开连接……close()
2.socket API
所有socket API需要导入头文件#include<arpa/inet.h>
int socket(int domain, int type, int protocol);
参数:
????????-domain:指定地址协议族。
????????常用取值:AF_INET(IPv4),AF_INET6(IPv6)
? ? ? ? -type:通信过程中使用的协议类型。
????????常用取值:SOCK_STREAM(流式协议),SOCK_DGRAM(报式协议)
? ? ? ? -protocol:具体使用的协议。
????????一般写0,os会根据前两个值自动推导出来。
返回值:
? ? ? ? 成功:返回套接字文件描述符
? ? ? ? 失败:-1
这里返回文件描述符又一次说明了socket的本质是文件。因此只要创建了socket,就可以在其他函数中通过其返回的文件描述符,像操作文件一样操作socket了。事实上其他函数也确实是这么做的。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
? ? ? ? -sockfd:用于连接的socket文件描述符。
? ? ? ? -addr:服务端进程的socket信息
? ? ? ? 这里形参使用struct sockaddr*类型,实际传入的实参一般不是这种类型,需要进行类型转换
? ? ? ? -addrlen:addr的长度
返回值:
? ? ? ? 失败:-1
int listen(int sockfd, int backlog);
参数:
? ? ? ? -sockfd:同上
? ? ? ? -backlog:请求队列长度
一个服务器进程可能会收到多个客户端请求,在上一个请求未完成之前新进来的请求会被放入一个队列结构的缓冲区,该缓冲区称为请求队列。
?返回值:
? ? ? ? 失败:-1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
? ? ? ? -sockfd:同上
? ? ? ? -addr:用于返回客户端的socket地址信息
? ? ? ? -addrlen:addr的长度
返回值:
????????成功:返回一个新的socket文件描述符,该文件描述符用于与addr所指客户端进行通信
? ? ? ? 失败:-1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:同bind()
返回值:
? ? ? ? 成功:0
? ? ? ? 失败:-1
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
参数:
? ? ? ? -fd:用于通信的socket文件描述符
? ? ? ? -buf:欲写/读的数据。write将数据写入写缓冲区,read从读缓冲区获取数据
? ? ? ? -count:buf长度
3.多并发服务器代码示例
该示例程序演示一个回声客户端,即客户端输入一条数据,服务端就返回一样的数据。
服务端每接受一次连接,就打印连接的客户端ip和端口号。
多个客户端可同时接入,服务端将会创建多个子进程为每个客户端请求新建socket维护通信。
服务端:server_process.c
//TCP通信多并发服务端
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
//创建socket
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1){
perror("socket");
exit(-1);
}
//绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}
//监听
ret = listen(lfd,128);
if(ret == -1){
perror("listen");
exit(-1);
}
//循环等待客户端连接
while(1){
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
//接受连接
int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
if(cfd == -1){
perror("accept");
exit(-1);
}
//每接到一个连接进入,则创建一个子进程于客户端通信
pid_t pid = fork();
if(pid == 0){
//子进程创建成功
//获取客户端的信息
char cliIP[16];
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,cliIP,sizeof(cliIP));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s,port is %d\n",cliIP,cliPort);
//接收客户端发来的数据
char recvBuf[1024] = {0};
while(1){
int len = read(cfd,&recvBuf,sizeof(recvBuf));
if(len == -1){
perror("read");
exit(-1);
}else if(len > 0){
printf("recv client data : %s\n",recvBuf);
}else{
printf("client closed...");
}
/*
第三个参数+1是为了将返回字符中的换行符带上,
否则如果两次发送的字符串长度不等,可能会返回
上一次遗留的字符
*/
write(cfd,recvBuf,strlen(recvBuf)+1);
}
close(cfd);
exit(0);
}
}
close(lfd);
return 0;
}
客户端:client.c
// TCP通信客户端
#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);
if(fd == -1)
{
perror("socket");
exit(-1);
}
//2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET,"192.168.59.129",&serveraddr.sin_addr.s_addr);
serveraddr.sin_port=htons(9999);
int ret = connect(fd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
if(ret == -1)
{
perror("connect");
exit(-1);
}
//3.通信
int mnum = 10;
char recvBuf[1024] = {0};
while(mnum > 0)
{
//客户端给服务器端发送数据
char * data;
printf("Input:");
scanf("%s",data);
write(fd,data,strlen(data)+1);//同服务端write函数一样,此处要+1传递换行符
//sleep(1);
//读取服务端发送的数据
int len = read(fd,recvBuf,sizeof(recvBuf));
if(len == -1)
{
perror("read");
exit(-1);
}else if(len > 0){//表示获取到了数据,len是获取的数据的长度
printf("recv server data: %s\n",recvBuf);
mnum--;
}else if(len == 0){//表示服务端断开连接
printf("server closed...\n");
break;
}
}
//4.关闭连接
close(fd);
return 0;
}
?
|