复习:
网络编程
重要知识点:
1 udp 和 tcp 的区别
2 如何实现udp传输
int fd = socket(, SOCK_DGRAM, );
bind(fd, (struct sockaddr *)&myaddr, sizeof(myaddr));
recvfrom();
sendto();
close(fd);
3 如何实现tcp 服务器
int fd = socket(, SOCK_STREAM, );
bind();
listen();
accept
recv
send
4 服务器多采用并发服务器
服务器有一个单独线程,accept, 只要有一个客户端连接,就先建一个线程,此线程单独recv
5 I/O 多路复用
select
6 udp 广播
192.168.31.255
setsockopt 允许广播
7 udp 组播
224.10.10.10
tcp / ip 3卷
卷1 协议的内容
卷2 协议的实现
面试题高级(比较难,代码理解即可):
1 抓取网络数据包
wireshark
注意: 安装过程中会提示 安装一个组件 winpcap (需要安装)
作用:
1) 实时分析数据报 : 捕获网卡数据
2) 打开历史报文
重点: wireshark问题
1.1 分析以太网包头(15min)
1.2 分析ip报文、tcp报文(15min)
重点: TCP 报文头格式
源端口号: 发送方使用的端口号(8888)
目的端口号: 接收方的端口号 (8888)
32位序号 : 报文的编号(1,2,3,4,5......)
32位确认号: 应答的编号(收到了哪个编号的数据)
4位首部长度:
6位报文类型:
ACK 应答报文
SYN 请求连接报文(connect + accept)
FIN 断开连接报文(close)
16位窗口大小:
因为发送方和接收方 有可能 网速不同, 接收方如果是一个慢速设备,
那么接收方用窗口大小提示发送方我还能收多少
16位校验和: 采用的是求和校验
重点
1.3 tcp连接的过程(accept 和 connect怎么搭配的)
使用的 tcp 3次握手
1.4 tcp 4次挥手
练习1: 客户端连接服务器时发送( C )包,服务器收到客户端连接请求发送( D )包
A ACK B FIN C SYN D SYN+ACK
练习2: 客户端和服务器的初始序号为(2000, 4000), 那么客户端SYN包序号为( A ), 服务器的SYN包序号为( C )
A 2000 B 2001 C 4000 D 4001
作业: 重画 3 次握手,4次挥手
2 客户端如何判断服务器断开
方法1: 通过recv的返回值 <= 0,如果 <=0 说明服务器出问题了
但recv 返回值 <= 0的判断可能会漏掉某些情况
方法2: 因为recv <= 0 并不能检测到所有断开情况,可以使用心跳包实现
原理: 客户端 定期(每隔1秒)给服务器发送1包数据,服务收到后回应,
如果客户端检测到,发送了多包数据,服务器仍然没有回应,说明,服务器出问题了,
客户端重连,直到服务器恢复为止
流程:
服务器功能: 等待客户端连接
一旦有客户端发送connect, 回应ok
客户端功能: 连接成功后,每隔1秒钟发送connect, 接收服务器的ok
如果长时间收不到ok, 客户端重新连接服务器
/client.c/
#include
#include
#include
#include
#include
int count = 3; //客户端接收不到服务器应答的次数,如果 == 0, 说明服务器不在线
int server_on = 0; //是否连接成功标志 0,断开 1 连接
int i = 0;
int main(int argc, char *argv[])
{
int fd;
struct sockaddr_in youaddr;
youaddr.sin_family = AF_INET;
youaddr.sin_port = htons(atoi(argv[2]));
youaddr.sin_addr.s_addr = inet_addr(argv[1]);
char buf[100] = "connect";
while(1)
{
sleep(1);
if(!server_on)
{
fd = socket(AF_INET, SOCK_STREAM, 0);
int ret = connect(fd, (struct sockaddr *)&youaddr, sizeof(youaddr));
if(ret == 0)
server_on = 1; //连接成功
else
{
printf("connect error %d\n", i);
close(fd); //连接失败,因为是无限循环,1秒钟后再重连
i++;
continue;
}
}
strcpy(buf, "connect");
send(fd, buf, sizeof(buf), 0); //发送connect
memset(buf, 0, sizeof(buf));
usleep(100000);
if(server_on)
{
if(recv(fd, buf, sizeof(buf), MSG_DONTWAIT) > 0)
{
printf("buf is %s\n", buf);
if(strcmp(buf, "ok") != 0) //没收到ok
{
count--;
if(count == 0)
{
printf("server is disconnect\n");
server_on = 0;
}
}
else
count = 3;
}
else
{
count = 0;
printf("server is no answer\n");
server_on = 0;
close(fd);
}
}
}
close(fd);
}
/server/
#include
#include
#include
#include
#include
int fd1;
int main()
{
int newfd, ret;
char buf[100] = { 0 };
int fd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(fd, F_SETFL, fcntl( fd, F_GETFD, 0 )|O_NONBLOCK );
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(55555);
myaddr.sin_addr.s_addr = htonl(INADDR_ANY); //192.168.20.252
bind(fd, (struct sockaddr *)&myaddr, sizeof(myaddr));
listen(fd, 5);
while(1)
{
newfd = accept(fd, NULL, NULL); //非阻塞等待客户端连接
if(newfd > 0)
{
printf("fd %d connect newfd %d\n", fd, newfd);
fd1 = newfd;
}
ret = recv(fd1, buf, sizeof(buf), MSG_DONTWAIT);//非阻塞等待客户端发送数据
if(ret > 0)
{
printf("recv %s\n", buf);
if(strcmp(buf, "connect") == 0) //收到了connect
{
strcpy(buf, "ok");
send(fd1, buf, sizeof(buf), MSG_DONTWAIT); //回应ok
}
}
usleep(100000); //0.1 s (usleep 是以微秒为单位)
}
close(fd);
close(newfd);
}
上面的服务器采用的是循环服务器,
循环服务器要求: accept 和 recv 都是 非阻塞的
非阻塞 方式接收数据:
二、非阻塞方式连接及接收(30min)
有些场合需要非阻塞
非阻塞: accept recv read ....
可以让这些函数不等
有时候,一个线程一直阻塞,效率有点低,
那么可以有两个解决办法
1) 不阻塞
2) 阻塞一小段时间(1秒)
2.1 如何实现非阻塞方式接收(15min)
文件I/O (不仅仅socket通信)
在打开文件后设置 O_NONBLOCK (非阻塞方式)
int fd = socket(AF_INET, SOCK_STREAM, 0); //socket描述符
1) int flags = fcntl(fd, F_GETFD, 0); //F_GETFD 取出socket默认属性, 给flag
fcntl(file ----> control), 文件控制
F_GETFD----> 读出文件处理方式标志位 (O_RDWR | O_TRUNC | .....)
取出文件标志位
2) fcntl(fd, F_SETFL, flags | O_NONBLOCK);
给文件标志位增加 O_NONBLOCK( 非阻塞 )
F_SETFL ---> 设置文件处理方式标志位
accept 函数变成非阻塞方式
flags (int 类型 32位)
bit0 bit1 bit2 bit3 bit4 .... bit31
可读 可写 新建 清空 非阻塞
flags = 0x05 ?-----> 0000 0101 可读 + 新建
如果再加上非阻塞权限
flags = flags | 0x10; //0001 0000
#define O_NONEBLOCK 0x10
flags = flags | O_NONEBLOCK;
2.2 非阻塞方式练习(15min)
参照上面两个函数,实现并验证accept 非阻塞模式
#include
#include
#include
#include
#include
int main()
{
int fd = socket(AF_INET, SOCK_STREAM, 0); //SOCK_STREAM 不要写成SOCK_DGRAM
int flags = fcntl(fd, F_GETFD, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(33333);
myaddr.sin_addr.s_addr = htonl(INADDR_ANY); //192.168.30.252
bind(fd, (struct sockaddr *)&myaddr, sizeof(myaddr));
listen(fd, 5); //设置同时连接最大值5个
int newfd = accept(fd, NULL, NULL); //非阻塞等待客户端连接
printf("11111111111111 %d\n", newfd); //-1 (正常 4), 因为没有客户端连接,所以-1
}
讲解:recv函数的第四个参数
recv / send
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
flags ---> 通常是0 以阻塞方式接收
如果以非阻塞方式接收数,flags 设置为 MSG_DONTWAIT
recv(newfd, buf, sizeof(buf), MSG_DONTWAIT);
/
3 线程池
多线程并发服务器 :
缺点: 如果有多个客户端
1) 耗费资源 (I/O 多路复用解决)
2) 线程 可能会频繁的创建,消除 (效率低) (线程池解决)
线程池原理:
先把线程创建好,然后线程先休眠,一旦有要执行的任务,唤醒线程
问题
1) 线程池解决什么问题 : 线程 会频繁的创建,消除
2) 如何实现 : 先把线程创建好,然后线程先休眠,一旦有要执行的任务,唤醒线程
线程以队列+链表方式进行管理
///线程池的实现//
#include
#include
#include
#include
#include
#include
/*组成:
线程池管理 用于创建及管理线程池
工作线程 线程池中的线程--用于处理执行任务
任务接口 提供处理任务的接口 供工作线程调度执行任务
任务队列 存放待处理的任务 工作线程从中取出任务并执行
*/
/*
*线程池里所有运行和等待的任务都是一个Thread_worker
*由于所有任务都在链表里,所以是一个链表结构
*/
typedef struct worker
{
/*回调函数,任务运行时会调用此函数,注意也可声明成其它形式*/
void *(*process) (void *arg); //函数指针
void *arg;/*回调函数的参数*/
struct worker *next;
} Thread_worker;
/*线程池结构*/
typedef struct
{
pthread_mutex_t queue_lock;
pthread_cond_t queue_ready;
/*链表结构,线程池中所有等待任务*/
Thread_worker *queue_head;//任务1-》任务2-》任务3
/*是否销毁线程池*/
int shutdown;
//线程池中所有线程的tid
pthread_t *threadid;
/*线程池中允许的活动线程数目*/
int max_thread_num;
/*当前等待队列的任务数目*/
int cur_queue_size;
} Thread_pool;
int pool_add_worker (void *(*process) (void *arg), void *arg);
void *thread_routine (void *arg);
static Thread_pool *pool = NULL;
void pool_init (int max_thread_num)
//创建线程池并初始化
//参数:线程池最大线程数、任务队列最大任务数
{
pool = (Thread_pool *) malloc (sizeof (Thread_pool));
pthread_mutex_init (&(pool->queue_lock), NULL);
pthread_cond_init (&(pool->queue_ready), NULL);
pool->queue_head = NULL;
pool->max_thread_num = max_thread_num;
pool->cur_queue_size = 0;
pool->shutdown = 0;
pool->threadid =(pthread_t *) malloc (max_thread_num * sizeof (pthread_t));
int i = 0;
for (i = 0; i < max_thread_num; i++)//批量创建线程
{
pthread_create (&(pool->threadid[i]), NULL, thread_routine,NULL);
}
}
/*向线程池中加入任务*/
//向任务队列中添加任务
//参数 任务指针
int pool_add_worker (void *(*process) (void *arg), void *arg)
{
/*构造一个新任务*/
Thread_worker *newworker =(Thread_worker *) malloc (sizeof (Thread_worker));
newworker->process = process;
newworker->arg = arg;
newworker->next = NULL;
pthread_mutex_lock (&(pool->queue_lock));
/*将任务加入到等待队列中*/
Thread_worker *member = pool->queue_head;
if (member != NULL)//对列 尾插 执行任务 头删
{
while (member->next != NULL)
member = member->next;
member->next = newworker;
}
else
{
pool->queue_head = newworker;
}
pool->cur_queue_size++;
pthread_mutex_unlock (&(pool->queue_lock));
//唤醒一个等待线程 如果所有线程都在忙碌,这句没有任何作用
// 唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是SCHED_OTHER类型的,系统将根据线程的优先级唤醒线程。
//如果没有线程被阻塞在条件变量上,那么调用pthread_cond_signal()将没有作用。
pthread_cond_signal (&(pool->queue_ready)); //发送信号给正在阻塞的线程,阻塞的线程解除阻塞,执行process
return 0;
}
/*销毁线程池,等待队列中的任务不会再被执行,但是正在运行的线程会一直
把任务运行完后再退出*/
int pool_destroy ()
{
if (pool->shutdown)
return -1;/*防止两次调用*/
pool->shutdown = 1;
//唤醒所有等待线程,线程池要销毁
pthread_cond_broadcast (&(pool->queue_ready));
/*阻塞等待线程退出,否则就成僵尸了*/
int i;
for (i = 0; i < pool->max_thread_num; i++)
{
pthread_join (pool->threadid[i], NULL);
}
free (pool->threadid);
/*销毁等待队列*/
Thread_worker *head = NULL;
while (pool->queue_head != NULL)
{
head = pool->queue_head;
pool->queue_head = pool->queue_head->next;
free (head);
}
/*条件变量和互斥量也别忘了销毁*/
pthread_mutex_destroy(&(pool->queue_lock));
pthread_cond_destroy(&(pool->queue_ready));
free (pool);
/*销毁后指针置空是个好习惯*/
pool=NULL;
return 0;
}
//工作线程的主要工作为处理任务,当任务队列不为空的时候,工作线程直接从队列头取出一个任务并执行;
//当任务队列为空的时候,工作线程将阻塞直到有任务添加进来;
void *thread_routine (void *arg)
{
printf ("starting thread 0x%x\n", (unsigned int)pthread_self ());
while (1)
{
pthread_mutex_lock (&(pool->queue_lock));
//如果等待队列为0并且不销毁线程池,则处于阻塞状态
while (pool->cur_queue_size == 0 && !pool->shutdown)
{
printf ("thread 0x%x is waiting\n", (unsigned int)pthread_self ());
pthread_cond_wait (&(pool->queue_ready), &(pool->queue_lock)); //让线程休眠
}
//线程池销毁
if (pool->shutdown)
{
pthread_mutex_unlock (&(pool->queue_lock));
printf ("thread 0x%x will exit\n", (unsigned int)pthread_self ());
pthread_exit (NULL);
}
printf ("thread 0x%x is starting to work\n", (unsigned int)pthread_self ());
//等待队列长度减去1,并取出链表中的头元素
pool->cur_queue_size--;
Thread_worker *worker = pool->queue_head;//头删 执行任务
pool->queue_head = worker->next;
pthread_mutex_unlock (&(pool->queue_lock));
/*调用回调函数,执行任务*/
(*(worker->process)) (worker->arg);
free (worker);
worker = NULL;
}
pthread_exit (NULL);
}
void *myprocess (void *arg)
{
printf ("threadid is 0x%x, working on task %d\n", (unsigned int)pthread_self (),*(int *) arg);
sleep (1);
return NULL;
}
int main (int argc, char **argv)
{
pool_init (3); //创建3个线程,默认是休眠的
int *workingnum = (int *) malloc (sizeof (int) * 10);
int i;
for (i = 0; i < 10; i++)
{
workingnum[i] = i;
pool_add_worker (myprocess, &workingnum[i]); //给线程添加任务,执行myprocess
}
sleep (5);
pool_destroy (); //销毁线程
free (workingnum);
return 0;
}
作业讲解:
一、ftp文件服务器作业讲解(60min) 取文件列表、下载文件、上传文件
用到的知识点:
目录操作(list)、文件I/O(get put)、为了支持多客户端,用到了多线程、socket通信(tcp)
公共结构体讲解
struct data_info
{
int type; //类型 1 list 2 get 3 put
char buf1[50]; //list 文件名 get 文件名 put 文件名
char buf2[500]; //list 无效 get 文件内容 put 文件内容
int len; //get put 文件长度
};
1.1 list的实现(15min)
客户端:
struct data_info s;
发送:
scanf("%s", buf); //list
if(buf 是 "list") //strcmp
{
s.type = 1;
send(fd, &s, sizeof(s), 0); //给服务器
}
收:
recv(fd, &s, sizeof(s), 0);
if(s.type == 1)
printf("%s\n", s.buf1);
服务器端:
struct data_info s;
接收:
recv(fd, &s, sizeof(s), 0);
if(s.type == 1)
{
dp = opendir("aaaa");
ep = readdir(dp);
while(ep != NULL)
{
strcpy(s.buf1, ep.d_name);
send(fd, &s, sizeo(s), 0);
ep = readdir(dp);
}
}
1.2 get file的实现(15min)
客户端:
发送:
scanf("%s%s", buf, file_name); //get hello.c
if(buf 是 get)
{
s.type = 2;
strcpy(s.buf1, file_name);
send(fd, &s, sizeof(s), 0);
FILE *fp = fopen("hello.c", "w");
}
接收:
recv(fd, &s, sizeof(s), 0);
if(s.type == 2)
{ //写入文件
FILE *fp = fopen("hello.c", "a+"); //追加写入
fwrite(s.buf2, 1, sizeof(s.buf2), fp);
}
服务器端:
接收:
recv(fd, &s, sizeof(s), 0);
if(s.type == 2)
{
FILE *fp = fopen("aaaa/hello.c", "r");
while(fread(s.buf2, 1, sizeof(s.buf2), fp) > 0)
send(fd, &s, sizeof(s), 0);
}
漏洞
1) "a+" 追加输入,如果客户端原来就有这个文件 ??(在get时,客户端先新建并清空文件)
2) 如果文件很小,发送的buf是500字节,那么如何处理 (双发发送时要用len 存储实际发送的字节数)
1.3 put file的实现(15min)
客户端:
发送:
scanf("%s%s", buf, file_name); //put a.c
if(buf 是 put)
{
s.type = 3;
strcpy(s.buf1, file_name);
FILE *fp = fopen("a.c", "r");
while(fread(s.buf2, 1, sizeof(buf2), fp) > 0)
send(fd, &s, sizeof(s), 0);
}
服务器端:
接收:
recv(fd, &s, sizeof(s), 0);
if(s.type == 3)
{
FILE *fp = fopen(s.buf1, "a+");
fwrite(s.buf2, 1, sizeof(s.buf2), fp);
}
/server.c//
#include
#include
#include
#include
#include
#include
struct data_info
{
int type; //1 list 2 get 3 put 4 quit
char buf1[50]; //保存文件名
char buf2[500]; //保存文件内容
int len; //加一个变量 number--->1 新建
};
void send_list(int fd, struct data_info *p)
{
DIR *dp = NULL; //定义一个结构体变量
dp = opendir("aaaa"); //打开目录,fopen:打开文件,返回FILE *
struct dirent *ep; //用来保存一个文件
ep = readdir(dp); //readdir,执行一次从目录中读出一个文件
//当读出所有文件时,返回值是NULL
while( ep != NULL)
{
if(ep->d_name[0] != '.') //以.开头是隐藏文件,不显示
{
strcpy(p->buf1, ep->d_name);
send(fd, p, sizeof(struct data_info), 0);
}
ep = readdir(dp);
}
}
void send_file(int fd, struct data_info *p)
{
char name[100] = { 0 };
sprintf(name, "%s/%s", "aaaa", p->buf1);
FILE *fp = fopen(name, "r");
printf("file name is %s\n", p->buf1);
if(fp != NULL)
{
int len;
while((len = fread(p->buf2, 1, 500, fp)) > 0) //p->len = fread(p->buf2, 1, 500, fp)
{
p->len = len;
send(fd, p, sizeof(struct data_info), 0);
}
fclose(fp);
}
}
void save_file(int fd, struct data_info *p)
{
char name[100] = { 0 };
sprintf(name, "%s/%s", "aaaa", p->buf1);
FILE *fp = fopen(name, "a+"); //有bug 追加方式写入,内容会不一致
printf("file name is %s\n", p->buf1);
if(fp != NULL)
{
fwrite(p->buf2,1,p->len,fp); //
fclose(fp);
}
strcpy(p->buf1, "success");
send(fd, p, sizeof(struct data_info), 0);
}
void *recv_fun(void *p)
{
int fd;
int *q = p;
fd = *q; //fd = *((int *)p);
struct data_info s;
char buf[100] = { 0 };
while(1)
{
if(recv(fd, &s, sizeof(s), 0) <= 0)
return 0;
printf("type is %d\n", s.type);
if(s.type == 1) //list
{
send_list(fd, &s);
}
else if(s.type == 2) //get
{
send_file(fd, &s);
}
else if(s.type == 3) //put
{
save_file(fd, &s);
}
}
}
int main(int argc, char *argv[])
{
pthread_t id;
char buf[100] = { 0 };
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(55555); //不要超过65535
myaddr.sin_addr.s_addr = htonl(INADDR_ANY); //inet_addr("127.0.0.1");
bind(fd, (struct sockaddr *)&myaddr, sizeof(myaddr));
listen(fd, 5);
while(1)
{
int newfd = accept(fd, NULL, NULL); //阻塞等待客户端连接
printf("client connect\n");
pthread_create(&id, NULL, recv_fun, &newfd); //一旦有客户端连接,就创建新线程
}
}
/client.c//
#include
#include
#include
#include
#include
struct data_info
{
int type; //1 list 2 get 3 put 4 quit
char buf1[50]; //保存文件名
char buf2[500]; //保存文件内容
int len;
};
int fd;
char buf[100] = { 0 };
void *recv_fun(void *p)
{
while(1)
{
struct data_info s;
int ret = recv(fd, &s, sizeof(s), 0); //阻塞
if(ret > 0)
{
if(s.type == 1) //list
{
printf("%s\n", s.buf1); //buf1 文件名
}
else if(s.type == 2)
{
FILE *fp = fopen(s.buf1, "a+"); //"r+" "w+"
//"a+" 以追加的方式打开文件并写入
if(fp != NULL)
{
fwrite(s.buf2,1,s.len,fp); //
fclose(fp);
}
}
}
}
}
void send_file(int fd, struct data_info *p)
{
FILE *fp = fopen(p->buf1, "r");
printf("file name is %s\n", p->buf1);
if(fp != NULL)
{
while((p->len = fread(p->buf2, 1, 500, fp)) > 0) //
{
send(fd, p, sizeof(struct data_info), 0);
}
fclose(fp);
}
}
int main(int argc, char *argv[])
{
pthread_t id;
fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in youaddr;
youaddr.sin_family = AF_INET;
youaddr.sin_port = htons(atoi(argv[2])); //不要超过65535
youaddr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址
connect(fd, (struct sockaddr *)&youaddr, sizeof(youaddr));
pthread_create(&id, NULL, recv_fun, NULL);
while(1)
{
struct data_info s;
gets(buf); //阻塞
if(strcmp(buf, "list") == 0) //strcmp 字符串比较
{
s.type = 1;
send(fd, &s, sizeof(s), 0);
}
else if(strcmp(buf, "get") == 0) //strcmp 字符串比较
{
s.type = 2;
gets(s.buf1);
FILE *fp = fopen(s.buf1, "w"); //清空并新建文件
fclose(fp);
send(fd, &s, sizeof(s), 0);
}
else if(strcmp(buf, "put") == 0) //strcmp 字符串比较
{
s.type = 3;
gets(s.buf1);
send_file(fd, &s);
}
}
}
新内容: 数据库