目录
在真实的服务器客户端模型中,服务器和客户端的关系应为1:n
单线程
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 将socket()返回值和本地的IP端口绑定到一起
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); // 大端端口
// INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
// 这个宏可以代表任意一个IP地址
addr.sin_addr.s_addr = INADDR_ANY; // 这个宏的值为0 == 0.0.0.0
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
// 3. 设置监听
ret = listen(lfd, 128);
// 4. 阻塞等待并接受客户端连接
struct sockaddr_in cliaddr;
int clilen = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
// 5. 和客户端通信
while(1)
{
// 接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = read(cfd, buf, sizeof(buf));
if(len > 0)
{
printf("客户端say: %s\n", buf);
write(cfd, buf, len);
}
else if(len == 0)
{
printf("客户端断开了连接...\n");
break;
}
else
{
perror("read");
break;
}
}
close(cfd);
close(lfd);
return 0;
}
在上面的代码中用到了三个会引起程序阻塞的函数,分别是:
- accept():如果服务器端没有新客户端连接,阻塞当前进程/线程,如果检测到新连接解除阻塞,建立连接
- read():如果通信的套接字对应的读缓冲区没有数据,阻塞当前进程/线程,检测到数据解除阻塞,接收数据
- write():如果通信的套接字写缓冲区被写满了,阻塞当前进程/线程(这种情况比较少见)
如果需要和发起新的连接请求的客户端建立连接,那么就必须在服务器端通过一个循环调用accept()函数,
另外已经和服务器建立连接的客户端需要和服务器通信,发送数据时的阻塞可以忽略,当接收不到数据时程序也会被阻塞,
这时候就会非常矛盾,被accept()阻塞就无法通信,被read()阻塞就无法和客户端建立新连接。
因此得出一个结论,基于上述处理方式,在单线程/单进程场景下,服务器是无法处理多连接的,解决方案也有很多,常用的有三种:
- 使用多线程实现
- 使用多进程实现
- 使用IO多路转接(复用)实现
- 使用IO多路转接 + 多线程实现
多线程
accept()函数,是一个线程,用于处理与客户端的连接
而每次建立连接,就需要多分配出一个线程来处理和客户端的通信
- 主线程:
- 负责监听,处理客户端的连接请求,也就是在父进程中循环调用accept()函数
- 创建子线程:建立一个新的连接,就创建一个新的子进程,让这个子进程和对应的客户端通信
- 回收子线程资源:由于回收需要调用阻塞函数,这样就会影响accept(),直接做线程分离即可。
- 子线程:负责通信,基于主线程建立新连接之后得到的文件描述符,和对应的客户端完成数据的接收和发送。
- 发送数据:send() / write()
- 接收数据:recv() / read()
注意:
- 同一地址空间中的多个线程的栈空间是独占的
- 多个线程共享全局数据区,堆区,以及内核区的文件描述符等资源,因此需要注意数据覆盖问题,并且在多个线程访问共享资源的时候,还需要进行线程同步。
#include <cstdio>
#include<arpa/inet.h>
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>
using namespace std;
void* working(void* arg);
#define MAXCLIENT 512
struct SockInfo
{
int fd; // 通信
pthread_t tid; // 线程ID
struct sockaddr_in addr; // 地址信息
};
struct SockInfo infos[MAXCLIENT];
int main()
{
//初始化
for (int i = 0; i < MAXCLIENT; i++)
{
bzero(infos, 0);
infos[i].fd = -1;
}
printf("%s 向你问好!\n", "SocketLinnx");
//1.创建监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
return -1;
}
//2.绑定本地端口
//2.1 addr结构体的初始化和地址
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;//指定为0后,自动读取实际ip
int ret = bind(fd, (sockaddr*)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("bind");
return -1;
}
//3.设置监听
ret = listen(fd, 128);
if (ret == -1)
{
perror("bind");
return -1;
}
//4.阻塞并等待客户端连接
int addrlen = sizeof(sockaddr_in);
while (1)
{
struct SockInfo* pinfo;
for (int i = 0; i < MAXCLIENT; i++)
{
if (infos[i].fd == -1)
{
pinfo = &infos[i];
break;
}
}
pinfo->fd = accept(fd, (struct sockaddr*)&pinfo->addr, (socklen_t*)&addrlen);
if (pinfo->fd == -1)
{
perror("accept");
//连接失败了就进入下一次循环
continue;
}
//创建对应子线程
pthread_t tid;
pthread_create(&tid, NULL, working, (void *)pinfo);
pthread_detach(tid);
}
close(fd);
return 0;
}
void * working(void* arg)
{
//建立连接成功,打印客户端IP和端口
//caddr是大端信息,需要转成小端输出
struct SockInfo* pinfo = (struct SockInfo*)arg;
char ip[32];
printf("客户端IP:%s ,端口:%d\n", inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(pinfo->addr.sin_port));
//5.进行通信
while (true)
{
//接收数据
char buff[1024];
int len = recv(pinfo->fd, buff, sizeof(buff), 0);
if (len > 0)
{
cout << buff << endl;
send(pinfo->fd, buff, len, 0);
}
else if (len == 0)
{
cout << "客户端断开连接" << endl;
break;
}
else {
perror("recv");
break;
}
}
//释放文件描述符和其他的东西
close(pinfo->fd);
pinfo->fd = -1;
return NULL;
}
标签:include,addr,int,fd,线程,服务器,多线程,客户端
From: https://www.cnblogs.com/liviayu/p/17787392.html