回顾:
UDP模型的UML图
TCP模型的UML图
案例1:
TCP的服务器(在案例中使用浏览器作为客户程序)
socket建立服务器的文件描述符号缓冲
bind把IP地址与端口设置到文件描述符号中
listen负责根据客户连接的不同IP与端口,负责生成对应的文件描述符号及其信息
accept一旦listen有新的描述符号产生就返回,否则阻塞。
// tcpserver.c
#include <stdio.h>
#include <stdlib.h>
#include < string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/ in.h>
#include <arpa/inet.h>
main()
{
int serverfd;
int cfd;
int a;
struct sockaddr_in sadr;
struct sockaddr_in cadr;
socklen_t len;
int r;
char buf[1024];
// 1.socket
serverfd=socket(AF_INET,SOCK_STREAM,0);
if(serverfd==-1) printf("1:%m\n"),exit(-1);
printf("建立服务器socket成功!\n");
// 2.bind
sadr.sin_family=AF_INET;
sadr.sin_port=htons(9999);
inet_aton("192.168.180.92",&sadr.sin_addr);
r=bind(serverfd,
( struct sockaddr*)&sadr, sizeof(sadr));
if(r==-1) printf("2:%m\n"),exit(-1);
printf("服务器地址绑定成功!\n");
// 3.listen
r=listen(serverfd,10);
if(r==-1) printf("3:%m\n"),exit(-1);
printf("监听服务器成功!\n");
// 4.accept
len= sizeof(cadr);
cfd=accept(serverfd,
( struct sockaddr*)&cadr,&len); // 每接受一个新的连接,就会返回一个新的文件描述符,来分辨是哪个连接。
printf("有人连接:%d,IP:%s:%u\n",
cfd,inet_ntoa(cadr.sin_addr),
ntohs(cadr.sin_port));
// 5.处理代理客户描述符号的数据
while(1)
{
r=recv(cfd,&a,4,MSG_WAITALL);
if(r>0)
{
// buf[r]=0;
printf("::%d\n",a);
}
if(r==0)
{
printf("连接断开!\n");
break;
}
if(r==-1)
{
printf("网络故障!\n");
break;
}
}
close(cfd);
close(serverfd);
}
案例2:
每个客户的代理描述符号的通信
二.TCP通信特点(相对于UDP)
案例3:
有连接:主要连接后,发送数据不用指定IP与端口
数据无边界:TCP数据流,非数据报文.
描述符号双工:
数据准确:TCP协议保证数据时完全正确
案例4:
使用TCP发送数据注意:
不要以为固定长的数据,一定接收正确,要求使用MSG_WAITALL(必须等待得到指定缓存长度recv才返回)
案例5:
TCP数据发送的分析:
定长数据:
基本数据int short long float double
结构体数据struct
建议使用MSG_WAITALL
不定长数据:
字符串数据以及文件数据等不固定长度的数据怎么发送?
制定数据包:
头:大小固定(数据大小)
体:大小变化(数据)
案例6:
使用TCP传送文件
定义文件数据包.
int 数据大小;
char[]数据
传递文件名
传递数据(循环)
传递0长度的数据表示文件结束
代码如下:
// demo1Client.c
// 发送端的代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include < string.h>
#include <sys/socket.h>
#include <netinet/ in.h>
#include <arpa/inet.h>
#include <fcntl.h>
main()
{
// 1. 建立socket
// 2. 连接到服务器
// 3. 打开文件
// 4. 发送文件名
// 5. 循环发送文件
// 6. 读取到文件尾,发送0数据包
int sfd; // socket描述符
int ffd; // 文件描述符
int size; // 读取和发送文件的长度
int r; // 函数返回值
int len; // 要发送的文件名的长度
char buf[128]; // 数据的缓存
struct sockaddr_in dr; // 网络地址
char filename[]="udp_a.c"; // 文件名
// 1.建立socket
sfd=socket(AF_INET,SOCK_STREAM,0);
if(sfd==-1)
printf("1:%m\n"),exit(-1);
printf("socket成功!\n");
// 2.连接到服务器
dr.sin_family=AF_INET;
dr.sin_port=htons(9988);
inet_aton("192.168.180.92",&dr.sin_addr);
r=connect(sfd,( struct sockaddr*)&dr, sizeof(dr));
if(r==-1)
printf("2:%m\n"),close(sfd),exit(-1);
printf("connect成功!\n");
// 3.打开文件
ffd=open(filename,O_RDONLY);
if(ffd==-1)
printf("3:%m\n"),close(sfd),exit(-1);
printf("open文件成功!\n");
// 4.发送文件名
len=strlen(filename);
r=send(sfd,&len, sizeof(len),0); // 发送文件名长度(告诉流,文件名占多长)
r=send(sfd,filename,len,0); // 发送文件名
if(r==-1)
printf("4:%m\n"),close(ffd),close(sfd),exit(-1);
printf("发送文件名成功!\n");
// 5.循环发送数据
while(1)
{
size=read(ffd,buf,128);
if(size==-1) break; // read错误,跳出循环
if(size==0) break; // 读到文件尾,跳出循环
if(size>0)
{
// 先发送数据的长度,再发送数据
// 发送数据长度
r=send(sfd,&size, sizeof(size),0);
if(r==-1) break;
r=send(sfd,buf,size,0); // 发送数据
if(r==-1) break;
}
}
// 6.读取到文件尾,发送0数据包
size=0;
r=send(sfd,&size, sizeof(size),0);
close(ffd);
close(sfd);
printf("OK!\n");
}
// demo1server.c
// 接收服务器的代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include < string.h>
#include <sys/socket.h>
#include <netinet/ in.h>
#include <arpa/inet.h>
#include <fcntl.h>
main()
{
// 1. 建立服务器socket
// 2. 绑定IP地址与端口
// 3. 监听
// 4. 接收连接
// 5. 接收文件名
// 6. 创建文件
// 7. 循环接收文件数据
int sfd, cfd, ffd;
int r;
int len;
char buf[128]; // 发送端定义的缓存大小是128,这里最好不要小于128
char filename[100];
struct sockaddr_in dr;
// 1. 建立服务器socket
sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1)
printf("1:%m\n"), exit(-1);
printf("socket服务器创建成功!\n");
// 2. 绑定IP地址与端口
dr.sin_family = AF_INET;
dr.sin_port = htons(9988);
inet_aton("192.168.180.92", &dr.sin_addr); // dr.sin_addr.s_addr = inet_addr("192.168.180.92");注意区别
r = bind(sfd, ( struct sockaddr*)&dr, sizeof(dr));
if(r == -1)
printf("2:%m"), close(sfd), exit(-1);
printf("绑定地址成功!\n");
// 3. 监听
r = listen(sfd, 10);
if(r == -1)
printf("3:%m\n"), close(sfd), exit(-1);
printf("监听成功!\n");
// 4. 接收连接
cfd = accept(sfd, 0, 0); // 这里我们不关心发送端的IP等信息,所以后面两个参数都为0。这里返回一个新的描述符,代表接收的连接
if(cfd == -1)
printf("4:%m\n"), close(sfd), exit(-1);
printf("开始接收文件!\n");
// 5. 接收文件名
r = recv(cfd, &len, sizeof(len), MSG_WAITALL); // 接收文件名的长度
r = recv(cfd, filename, len, MSG_WAITALL); // 根据文件名长度,接收文件名
filename[r] = 0; // 在文件名后面加结束符
// 6. 创建文件
ffd = open(filename, O_CREAT|O_RDWR, 0666); // 如果文件存在,直接覆盖.不要和发送文件放在同一个目录运行,会覆盖发送文件
if(ffd == -1)
printf("6:%m\n"), close(sfd), close(cfd), exit(-1);
printf("创建文件成功!\n");
// 7. 循环接收文件数据
while(1)
{
r = recv(cfd, &len, sizeof(len), MSG_WAITALL);
if(len == 0)
break; // 长度为0,表示文件传送完毕的信号
r = recv(cfd, buf, len, MGS_WAITALL);
r = write(ffd, buf, len);
}
close(ffd);
close(cfd);
close(sfd);
printf("接收数据完毕!\n");
}
PS:UDP面向无连接,TCP面向连接,所以推荐UDP不用connect,直接sendto, 而TCP则先连接,然后send,而不是sendto。
三.TCP服务器编程模式
TCP的服务器端维护多个客户的网络文件描述符号.
对服务器多个客户描述符号同时做读操作,是不可能.需要多任务模型完成.
多任务模型?
1.多进程
2.IO的异步模式(select模式/poll模式)
3.多线程模式
4.多进程池
5.线程池
四.综合应用--多进程应用
1.怎样使用多进程
2.多进程的缺陷,以及怎么解决
小例子:用TCP写一个聊天程序
客户端
2.1.建立socket
2.2.连接服务器
2.3.创建CURSES界面
2.4.创建子进程
2.5.在父进程中,输入,发送聊天信息
2.6.在子进程中,接收服务器传递其他客户聊天信息
// chatclient.c
// 聊天程序客户端
#include <stdio.h>
#include <stdlib.h>
#include < string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/ in.h>
#include <arpa/inet.h>
#include <curses.h>
#include <signal.h>
WINDOW*winfo,*wmsg;
int fd;
int r;
struct sockaddr_in dr;
int isover=1;
int initSocket(); // 初始化:创建描述符,绑定IP
void initUI(); // 初始化curses界面
void destroy(); // 清理:释放UI, 关闭网络
void handle( int s)
{
int status;
wait(&status);
destroy();
exit(-1);
}
main()
{
// printf("网络初始化成功!\n");
initUI();
r=initSocket();
if(r==-1) exit(-1);
signal(SIGCHLD,handle);
if(fork())
{
// 父进程,输入,发送
char buf[256];
while(1)
{
mvwgetstr(wmsg,1,1,buf);
// buf[r]=0;
send(fd,buf,strlen(buf),0);
// wclear(wmsg);
// box(wmsg,0,0);
refresh();
wrefresh(wmsg);
wrefresh(winfo);
}
}
else
{
// 子进程,接收,显示
char buf[256];
int line=1;
while(1)
{
r=recv(fd,buf,255,0);
if(r==-1) break;
if(r==0) break;
buf[r]=0;
mvwaddstr(winfo,line,1,buf);
line++;
if(line>=(LINES-3))
{
wclear(winfo);
line=1;
box(winfo,0,0);
}
wmove(wmsg,1,1);
touchwin(wmsg);
refresh();
wrefresh(winfo);
wrefresh(wmsg);
}
exit(-1);
}
destroy();
}
void destroy()
{
close(fd);
endwin();
}
void initUI()
{
initscr();
winfo=derwin(stdscr,(LINES-3),COLS,0,0);
wmsg=derwin(stdscr,3,COLS,LINES-3,0);
keypad(stdscr,TRUE);
keypad(wmsg,TRUE);
keypad(winfo,TRUE);
box(winfo,0,0);
box(wmsg,0,0);
refresh();
wrefresh(winfo);
wrefresh(wmsg);
}
int initSocket()
{
fd=socket(AF_INET,SOCK_STREAM,0);
if(fd==-1) return -1;
dr.sin_family=AF_INET;
dr.sin_port=htons(9989);
dr.sin_addr.s_addr=inet_addr("192.168.180.92");
r=connect(fd,( struct sockaddr*)&dr, sizeof(dr));
if(r==-1)
{
close(fd);
return -1;
}
return 0; // fd是全局变量,不用返回。初始化成功,返回0
}
// chatserver.c
// 聊天程序服务器端
#include <stdio.h>
#include <stdlib.h>
#include < string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/ in.h>
#include <arpa/inet.h>
#include <sys/mman.h>
int sfd;
int *fds; // 存放所有客户代理描述符号
int idx=0; // 客户在数组中下标
struct sockaddr_in dr;
int r;
main()
{
// 1. 建立服务器socket
// 2. 绑定地址
// 3. 监听
// 4. 循环接收客户连接
// 5. 建立一个子进程
// 6. 子进程任务:接收客户数据并且广播
// 1.建立服务器 socket
fds=mmap(0,4*100,PROT_READ|PROT_WRITE,
MAP_ANONYMOUS|MAP_SHARED,0,0);
bzero(fds, sizeof(fds));
sfd=socket(AF_INET,SOCK_STREAM,0);
if(sfd==-1) printf("1:%m\n"),exit(-1);
printf("socket OK!\n");
// 2.绑定地址
dr.sin_family=AF_INET;
dr.sin_port=htons(9989);
dr.sin_addr.s_addr=inet_addr("192.168.180.92");
r=bind(sfd,( struct sockaddr*)&dr, sizeof(dr));
if(r==-1) printf("2:%m\n"),exit(-1);
printf("bind ok!\n");
// 3.监听
r=listen(sfd,10);
if(r==-1) printf("3:%m\n"),exit(-1);
printf("listen ok!\n");
// 4.循环接收客户连接
while(1)
{
fds[idx]=accept(sfd,0,0);
if(fds[idx]==-1) break;
printf("有客户连接:%d\n",fds[idx]);
// 5.建立一个子进程
if(fork())
{
idx++;
continue;
}
else
{
// 6.子进程任务:接收客户数据并且广播
char buf[256];
int i;
printf("开始接收客户数据:%d\n",fds[idx]);
while(1)
{
// 接收客户数据
r=recv(fds[idx],buf,255,0);
printf("%d\n",r);
if(r==0)
{
printf("有客户退出\n");
close(fds[idx]);
fds[idx]=0;
break;
}
if(r==-1)
{
printf("网络故障\n");
close(fds[idx]);
fds[idx]=0;
break;
}
buf[r]=0;
printf("来自客户的数据:%s\n",buf);
// 广播
for(i=0;i<100;i++)
{
if(fds[i]>0)
{
send(fds[i],buf,r,0);
}
}
}
exit(0);
}
}
close(sfd);
}
总结:
建立socket
绑定地址
监听
循环接收客户连接
为客户创建子进程
在子进程接收该客户的数据,并且广播
总结:
1.TCP的四大特点
2.TCP的数据接收:固定长与变长数据的接收
3.TCP的服务器多进程处理
问题:多进程由于进程资源结构独立.
新进程的文件描述符号的环境在老进程无法访问?
作业:
思考:
有什么编程技巧可以解决进程的文件描述符号的一致?
作业:
完成TCP的聊天程序.
1.数据能运行
2.处理僵死进程
3.服务器退出,客户也能正常结束
4.客户退出,服务器也能够正确结束客户连接.