如果有多个客户端与服务器发生请求,有两种处理方式,一是顺序处理,二是并发处理(多线程)
字节序转换
数据在计算机中存储时是小端序,但通常情况下网络字节序采用的是大端序,所以需要字节序转换
(PS:之所以采用大端序是因为早期的网络协议使用的就是大端序,后续为了统一就沿用大端序了)
uint16_t htons(uint16_t hostshort);//短整型 主机->网络
uint32_t htonl(uint32_t hostlong);//整型 主机->网络
uint16_t ntohs(uint16_t netshort);//短整型 网络->主机
uint32_t ntohl(uint32_t netlong);//整形 网络->主机
IP地址转换
虽然IP地址本质上是一个整形术,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换
下面两个函数均支持IPV4和IPV6
int inet_pton(int af,const char*src,void *dst);
反过来
#include<arpa/inet.h>
const char*inet_ntop(int af,const void *dst ,socklen_t size);//将大端的整型数,转换为小端的点分十进制的IP地址
TCP通信流程
服务器端通信流程
1.创建用于监听的套接字
int lfd=socket();
2.将得到的监听文件描述符和本地的IP端口进行绑定
bind();
3.设置监听(成功之后开始监听,监听的是客户端的连接)
listen();
4.等待并接受客户端的连接请求,建立新的连接,会得到一个新的文件描述符(通信的),没有新连接请求就阻塞
int cfd=accept();
5.通信
read();
write();
6.断开连接,关闭套接字
close();
客户端端通信流程
1.创建一个通信的套接字
int lfd=socket();
2.连接服务器,需要知道服务器绑定的IP和端口
connect();
3.通信
4.断开连接
close();
要先启服务器端的sokect把套接字声明出来,然后再让客户端通信
接收数据的函数
如果连接没有断开,接收端接受不到数据,接受数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接受数据,当发送端断开连接,接收单无法接收到任何数据,这是就不会阻塞了,函数直接返回
ssize_t read(int sockfd,void *buf,size_t size);
ssize_t recv(int sockfd,void *buf,size_t size,int flags)
发送数据的函数
ssize_t write(int fd,const void *buf,size_t len);
ssize_t send(int fd,const void *buf,size_t len,int flags);
初始化套接字环境
include <winsock2.h>
ws2_32.dll
在windows中使用套接字需要先加载套接字库(套接字环境),最后释放套接字资源
WSAStartup(WORD wVersionRequested,LPWSDATA lpWSAData);
//wVersionRequested指定了请求的Win版本
//lpWSAData是一个指针类型
//调用成功则返回0,调用失败返回SOCKET_ERROR
使用完之后需要注销
int WSACleanup(void)
使用案例:
WSAData wsa;
//初始化套接字
WSAStartup(MAKEWORD(2,2),&wsa);
//...
//注销
WSACleanup();
文件描述符
服务器有两个文件描述符。一个用来监听,一个用来通信,客户端只有一个文件描述符用来通信
文件描述符对应的内存结构:一块内存是读缓冲区, 一块内存是写缓冲区
套接字通信的服务器端的实现
# include <stdio.h>
#include<stdlib.h>
#include<unistd.h> // Unix系统
#include<string.h>
#include<arpa/inet.h>//Unix/Linux 系统上常见的头文件
int main() {
//1.创建监听的套接字
int fd = socket(AD_INET,sock_stream,0);//第一个参数指定IPV4,第二个参数:基于TCP,采用流式写,第三个参数表示让系统根据第一个参数自动选择合适的协议
if (fd == -1)//如果创建失败
{
perror("socket");//打印错误信息
return -1;
}
//2.绑定本地的IP port
struct sockaddr_in saddr;
saddr.sin_family = AF_inet;
saddr.sin_port = htons(9999);//进行大端转换
saddr.sin_addr.s_addr = inaddr_any;//绑定零地址,在服务器段会自动读网卡的实际IP
int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));//第二个参数的含义是指定要绑定的地址信息,包括 IP 地址和端口号。
if (ret == -1)
{
perror("bind");
return -1;
}
//3.设置监听
ret = listen(fd, 128);
if (ret == -1)
{
perror("listen");
return -1;
}
//4.阻塞并等待客户端连接
struct sockaddr_in caddr;
int addrlen = sizeof(caddr);
int cfd = accept(fd, (struct sockaddr*)&caddr, &addrlen);
//连接建立成功,打印客户端的IP和端口信息
//这里使用小端
//5.通信
while (1) {
//接收数据
char buff[1024];
int len = recv(cfd, buff, sizeof(buf), 0);
if (len > 0)
{
printf("client say: %s\n", buff);
send(cfd, buff, len, 0);
}
else if (len == 0)
{
//输出断开连接
}
else
{
perror("recv");//这里说明是读取文件失败
break;
}
}
//关闭文件描述符
close(fd);
close(cfd);
return 0;
}
套接字客户端的实现
# include <stdio.h>
#include<stdlib.h>
#include<unistd.h> // Unix系统
#include<string.h>
#include<arpa/inet.h>//Unix/Linux 系统上常见的头文件
int main() {
//1.创建通信的套接字
int fd = socket(AD_INET,sock_stream,0);//第一个参数指定IPV4,第二个参数:基于TCP,采用流式写,第三个参数表示让系统根据第一个参数自动选择合适的协议
if (fd == -1)//如果创建失败
{
perror("socket");//打印错误信息
return -1;
}
//2.连接服务器的IP port
struct sockaddr_in saddr;
saddr.sin_family = AF_inet;
saddr.sin_port = htons(9999);//进行大端转换
inet_pton(AF_INET, "192.168.xxx.xxx", &saddr.sin_addr.s_addr);
//saddr.sin_addr.s_addr = inaddr_any;//绑定零地址,在服务器段会自动读网卡的实际IP
int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
//int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));//第二个参数的含义是指定要绑定的地址信息,包括 IP 地址和端口号。
if (ret == -1)
{
//perror("bind");
perror("connecy");
return -1;
}
//3.设置监听
//ret = listen(fd, 128);
//if (ret == -1)
//{
// perror("listen");
// return -1;
//}
//4.阻塞并等待客户端连接
//struct sockaddr_in caddr;
//int addrlen = sizeof(caddr);
//int cfd = accept(fd, (struct sockaddr*)&caddr, &addrlen);
//连接建立成功,打印客户端的IP和端口信息
//这里使用小端
//5.通信
int number = 0;
while (1) {
//发送数据
char buff[1024];
sprintf(buff, %d...\n, number++);
send(fd, buff, strlen(buff) + 1, 0);
//接收数据
memset(buf, 0, sizeof(buf));
int len = recv(cfd, buff, sizeof(buf), 0);
if (len > 0)
{
printf("server say: %s\n", buff);
send(cfd, buff, len, 0);
}
else if (len == 0)
{
//输出断开连接
}
else
{
perror("recv");//这里说明是读取文件失败
break;
}
}
//关闭文件描述符
close(fd);
//close(cfd);
return 0;
}
但是我们之前做的只能进行单线程/进程的操作,服务器无法处理多连接,解决办法有:
- 使用多线程
- 使用多进程
- 使用IO多路转接(复用)
- 使用IO多路转接-多线程实现
我们先来研究多线程,主线程(一个)调用accept,用来检测与客户端的连接,子线程(多个)用来与客户端的通信
# include <stdio.h>
#include<stdlib.h>
#include<unistd.h> // Unix系统
#include<string.h>
#include<arpa/inet.h>//Unix/Linux 系统上常见的头文件
//信息结构体
struct SockInfo {
struct sockaddr_in addr;
int fd;
};
struct SockInfo infos[512]; //目前设置只能和512个客户端连接
void working(void* arg);
int main() {
int fd = socket(AD_INET, sock_stream, 0);
if (fd == -1)
{
perror("socket");
return -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(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("bind");
return -1;
}
ret = listen(fd, 128);
if (ret == -1)
{
perror("listen");
return -1;
}
//初始化结构体数组
int max = sizeof(infos) / sizeof(infos[0]);
for (int i = 0; i < max; i++) {
bzeo(&info[i], sizeof(infos[i]));
infos[i].fd = -1;//不为-1说明被占用了
}
//struct sockaddr_in caddr;
int addrlen = sizeof(struct sockaddr_in);
while (1)//这里添加循环,持续监听并接受客户端的连接
{
struct Sockinfo* pinfo;
for (int i = 0; i < max; ++i) {//看看哪个文件描述符被占用
if (infos[i].fd == -1) {
pinfo = &infos[i];
break;
}
}
int cfd = accept(fd, (struct sockaddr*)&pinfo->addr, &addrlen);
pinfo->fd = cfd;
if (cfd == -1)
{
peerror("accept");
break;
}
//创建子线程
pthread_t tid;
pthread_create(&tid, NULL, working, pinfo);//执行working里的任务
pthread_detach(tid);//如果使用join,主线程在working中会一直阻塞,没办法再监听连接,所以要分离
}
close(fd);
return 0;
}
void* working(void* arg)
{
struct Sockinfo* pinfo = (struct Sockinfo*)arg;
while (1) {
char buff[1024];
int len = recv(pinfo->fd, buff, sizeof(buf), 0);
if (len > 0)
{
printf("client say: %s\n", buff);
send(pinfo->fd, buff, len, 0);
}
else if (len == 0)
{
}
else
{
perror("recv");
break;
}
}
close(pinfo->fd);
pinfo->fd = -1;
return NULL;
}
在套接字服务器端使用线程池的思路,主要分为三个部分
1.任务队列,存储需要处理的任务,由工作的线程处理这些任务
- 通过线程池提供的API函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
2.工作的线程(任务队列任务的消费者)n个
- 线程池中维护了一定数量的工作线程,他们的作用是不停的读任务队列,从里面取出任务并处理
3.管理者线程(不处理任务队列中的任务)1个
- 他的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测