一、UDP
1、总结:
server:
(1)创建socket
(2)绑定端口号
(3)循环的读取数据
(4)针对读取到的数据进行计算与处理
(5)把处理后的结果发送给客户端
client:
(1)创建socket
(2)给服务器发送请求
(3)从服务器中读取结果
2、实现代码:
服务器端
1、尝试从socket中读取数据(进行一系列的计算)
2、把字符串原封不动的写会数据端
//端口号的绑定(1025-65535)[1-1024为知名端口号,预留给操作系统,若要使用,需要在root用户下]
//./server 192.168.80.132 9090
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
# include<netinet/in.h>
int main(int argc,char* argv[])
{
if(argc != 3)//判断命令行参数够不够
{
printf("Usage ./server [ip] [port]\n");
return 1;//退出,结果错误
}
//创建socket文件描述符
int fd = socket(AF_INET,SOCK_DGRAM,0);//AF_INET:IPV4版本;SOCK_DGRAM:数据报
if(fd < 0)//文件描述符是有限的,创建完了,则失败。文件描述符可通过(ulimit -a)查看,
//查看的结果都保存PCB中,bash进程可继承。也可以通过(ulimit -n 个数)来进行修改。所以可以通过ulimit查看并修改文件描述符的个数
//对于一个进程,栈有多大,同ulimit
{
perror("socket");
return 1;
}
//把点分十进制的ip地址转化为uint-32的ip地址
//把主机序转化为网络字符序
struct sockaddr_in addr;
addr.sin_family = AF_INET;//添加协议族
addr.sin_addr.s_addr = inet_addr(argv[1]);//把点分十进制的ip地址转化为uint-32的ip地址
addr.sin_port = htons(atoi(argv[2]));//把主机序转化为网络字符序
int ret = bind(fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret < 0){//如果一个端口号已被替换,则bind失败
perror("bind");
return 1;
}
while(1){//死循环,反复从客户端接收信息
//读
char buf[1024] = {0};
struct sockaddr_in peer;//对端的地址,输出型参数,不用进行初始化
socklen_t len = sizeof(peer);
ssize_t read_size = recvfrom(fd,buf,sizeof(buf) - 1,0,(struct sockaddr*)&peer,&len);//
//recvfrom中第三个参数为缓冲区的长度;第四个参数叫做flag默认为0;第五个(输出型的参数,表示
ip地址的端口号,一旦返回就可以把客户端的ip地址和端口号拿到结构中,获取对端的i地址和端口号)和第六个参数(输入输出型参数:传入时,返回缓冲区的长度;传出时返回结构体的长度)
//recfrom的返回值为ssize_t:表示有符号的长整型
if(read_size < 0)//执行失败,继续执行下一步
{
//filure
perror("recvfrom");
continue;
}
buf[read_size] = '\0';//双重保证,保证缓冲区中的内容以'\0'结束
printf("client %s:%d say:%s\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),buf);//接受完毕
//inet_ntoa:从点分十进制的ip地址转换为字符串
//ntohs(peer.sin_port):返回端口号
//返回客户端:发送多大,接受多大的值,所以使用strlen,而不是使用sizeof
sendto(fd,buf,strlen(buf),0,(struct sockaddr*)&peer,len);//len:输入型参数,不用取地址//struct sockaddr*:指针强转
}
close(fd);
return 0;
}
客户端代码:
# include<stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<sys/socket.h>
# include<arpa/inet.h>
# include<netinet/in.h>
// ./client 127.0.0.1 9090 127.0.0.1:环回ip自己的ip【发送数据、接收数据的人都是自己】
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage ./server [ip] [port]\n");
return 1;
}
//创建socket
int fd = socket(AF_INET,SOCK_DGRAM,0);
if(fd < 0)
{
perror("socket");
return 1;
}
struct sockaddr_in server_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=inet_addr(argv[1]);
server_addr.sin_port=htons(atoi(argv[2]));
//客户端不用绑定端口号,是因为操作系统会在程序运行时自动分配端口号。
//由于客户端的机器装了哪些程序不确定。如果强行绑定端口号,就可能和客户端的
//程序冲突,操作系统就会在通信时自动分配一个端口号。
//服务器端必须绑定端口号,如果不绑定,操作系统自动分配,客户端不知道
//端口号是谁,则无法发送请求。所以服务器一定要绑定端口号。
//进行数据传输
while(1){
char buf[1024] = {0};
//调用read函数从标准输入读数据
ssize_t read_size =read(fd,buf,sizeof(buf) - 1);//
if(read_size < 0)
{
perror("recvfrom");
return 1;
}
if(read_size==0){
//read返回0意味着读到了eof文件结束标志,读完了
printf("read done\n");
return 0;//代码执行完毕,并且结果正确
}
buf[read_size]='\0';
sendto(fd,buf,strlen(buf),0,(struct sockaddr*)&server_addr,sizeof(server_addr));
//读数据
char buf_output[1024]={0};
read_size=recvfrom(fd,buf_output,sizeof(buf_output)-1,0,NULL,NULL);
//最后两个参数为空是因为一定知道是从哪儿来的,一定是经过server服务器来的
//服务器的ip地址和端口号已经得到了
if(read_size<0){
perror("recvfrom");
return 1;
}
buf_output[read_size]='\0';
printf("server response :%s\n",buf_output);
}
close(fd);
return 0;
}
//服务器:循环的读数据,然后将数据写会socket
//客户端:循环的从标准输入读,写给服务器,然后再尝试从服务器读,最后写到标准输出
//
makefile中的代码:
.PHONY:all
all:server client
server:server.c
gcc $^ -o $@
client:client.c
gcc $^ -o $@
.PHONY:clean
clean:
rm server client
先启动服务器,因为服务器是等待接收数据(若是先启动客户端,发送的数据不知道发送给谁了)
直接输入./server,提示打开方式不对:应该说明IP地址和端口号
输入ip地址和端口号后:
服务器启动完毕
验证服务器启动完毕:重新开一个窗口输入命令:netstat -anp | grep 9090
全0的ip地址表示任何的IP地址,相当于通配符。0.0.0.0:*表示对ip地址和端口号都没有限制
在ip地址相同的情况下,不能同时启动多个端口号也相同的服务器端(同样的ip地址和端口号不能被一起绑定)
启动客户端:./client ip地址 端口号
此处的IP地址和端口号必须和服务器端的IP地址和端口号相同,如果不匹配,则无法运行结果
在虚拟机上进行测试:
(1)设置---网络适配器必须设置为桥接模式;
(2)还必须保证服务器和客户端对应的主机在同一个局域网内【两台主机都在同一个路由器上,同一个手机热点】
(3)关闭服务器端的防火墙(在网络安全的角度上进制一些有嫌疑的程序、非法的程序访问主机
虚拟机的IP地址也是局域网的IP地址,只能在局域网内使用,或者使用云服务器。
*******拷贝到云服务器的方式:
(1)sz 程序名:把这个程序先拷贝到桌面上,将这个可执行程序从桌面拖到云服务器上;再加上可执行权限
;接着再云服务器中输入:./server 0 9090 此时的0表示任何一个IP地址【一个主机可能会有多个IP地址,,从任何一个IP地址过来都可以】
./client IP地址 此时的IP地址是指服务器的外网IP
虚拟机的IP地址和云服务器感知的IP地址不同,是因为NAT机制
TCP:
服务器端:
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<arpa/inet.h>
# include<sys/socket.h>
# include<string.h>
# include<netinet/in.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
//./server 0 9090
int main(int argc,char* argva[])
{
if(argc!=3){
printf("Usage ./server [ip][port]\n");
return 1;
}
//创建socket
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd<0)
{
perror("socket");
return 1;
}
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(argv[1]);
E> addr.sin_port=htons(atoi(argv[2]));
int ret=bind(fd,(sockaddr*)&addr,sizeof(addr));
if(ret<0){
perror("bind");
return 1;
}
//把tcp socket变成被动的模式。此时才能允许客户端来建立连接
ret=listen(fd,5);
if(fd<0){
perror("listen");
return 1;
}
while(1){
sockaddr_in peer;
socklen_t len=sizeof(peer);
int new_fd=accept(fd,(sockaddr *)&peer,&len);
if(new_fd<0){
perror("accept");
continue;
}
while(1){
char buf[1024]={0};
ssize_t read_size=read(new_fd,buf,strlen(buf));
if(read_size<0){
perror("read");
return 1;
}
if(read_size==0)
{
//读到了EOF,对于此场景下,意味着客户端关闭了 socket;
printf("read done");
//此处这个关闭,一定不要忘记,否则就会出现文件描述符的泄漏
//并且在tcp中能够看到close_wait状态
close(new_fd);
break;
}
buf[read_size]='\0';
printf("client %s:%d say:%s\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),buf);
write(fd,buf,strlen(buf));
}
}
return 0;
}
客户端:
# include<stdio.h>
# include<stdlib.h>
# include<string.h>
# include<sys/socket.h>
# include<netinet/in.h>
# include<arpa/inet.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage ./server[ip][port]");
return 1;
}
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd<0)
{
perror("socket");
return 1;
}
sockaddr_in server_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=inet_addr(argv[1]);
server_addr.sin_port=htons(atoi(argv[2]));
int ret=connect(fd,(sockaddr*)&server_addr,sizeof(server_addr));
if(ret<0){
perro("connect");
return 1;
}
while(1){
char buf[1024]={0};
ssize_t read_size=read(0,buf,sizeof(buf)-1);
if(read_size<0)
{
perror("read");
return 1;
}
if(read_size==0){
printf("read done");
return 0;
}
buf[read_size]='\0';
write(fd,buf,strlen(buf));
char buf_output[1024]={0};
read_size=read(fd,buf_output,sizeof(buf_output)-1);
if(read_size<0){
perror("read");
return 1;
}
if(read_size==0){
//服务器率先关闭了new_sock
printf("read done");
return 0;
}
buf_output[read_size]='\0';
printf("server resp:%s\n",buf_output);
}
close(fd);
return 0;
}
总结:
客户端:
1、从标准输入中读取用户输入
2、把这个字符串通过socket传递给服务器
3、尝试从服务器读取数据
4、把从服务器读取到的数据显示到标准输出上
//客户端一般不用绑定端口号,操作系统在程序运行时,自动分配。由于客户端机器装了哪些程序不确定。
//如果强行绑定端口号,就可能和客户端的其他程序冲突
//数据传输
//read等于0表示读到了EOF
//端口号通过服务器读来,客户端知道,所以不用设置端口号,置为NULL
二、TCP
实现tcp多进程和多线程的版本,多线程的系统开销比较小,但是反复创建进程和线程的情况下,还是存在很大的开销。
还可以用生产者,消费者模型构建,用一个专门的线程作为生成者模型,只用负责从socket中读取数据,放到一个队列里;有若干个消费者线程来复用队列中的数据,通过队列中的数据进行一系列计算。
1、TCP协议的特点:
1)、有连接;TCP建立连接、断开连接的过程(重要)
TCP建立连接的过程:
1‘两次握手:可能会发生丢包
2’三次握手:发送方发送信息,对方返回信息(收到这个消息,对这个消息的应答),发送方发送收到了响应的消息。【发送方与服务器端建立连接,服务器端进行响应;服务器端和发送端建立连接,发送端对服务器端进行响应;双方都尝试对对方进行连接,并进行响应】
TCP断开连接的过程:四次挥手:
1‘
2)、可靠传输
3)、面向字节流
2、小节:
server:
(1)创建socket
(2)绑定端口号
(3)把socket转换成被动模式(listen)
(4)循环的进行accept
(5)从accept返回的new_fd中读取客户端的请求
(6)根据读取到的请求进行计算和处理
(7)把处理后的结果发送给客户端
【多进程,多线程,IO多路复用】
client:
(1)创建socket
(2)和服务器建立连接
(3)给服务器发送数据
(4)从服务器读取返回的结果
(5)和服务器断开连接
3、实现代码:
客户端
服务器端:
缺点
改进
网络传输
基于socket实现一个联机对战的三子棋: