一、Socket简介
1.1 什么是socket
socket通常也称作"套接字",⽤于描述IP地址和端⼝,是⼀个通信链的句柄,应⽤
程序通常通过"套接字"向⽹络发出请求或者应答⽹络请求。⽹络通信就是两个进程
间的通信,这两个进程之间是如何识别彼此的呢?那就是套接字(Socket),每个
套接字由⼀个 IP 地址和⼀个端⼝号组成。Socket起源于Unix,⽽Unix/Linux基本
哲学之⼀就是“⼀切皆⽂件”,对于⽂件⽤【打开】【读写】【关闭】模式来操作
socket是⼀种特殊的⽂件,⼀些socket函数就是对其进⾏的操作(打开、读/写
IO、关闭)。
1.2 网络字节序
主机字节序:就是⾃⼰的主机内部,内存中数据的存放⽅式,可以分为两种:1.⼤端字节序(big-endian):按照内存的增⻓⽅向,⾼位数据存储于低位内存中2.⼩端字节序(little-endian):按照内存的增⻓⽅向,⾼位数据存储于⾼位内存中。⼤多数Intel兼容机都采⽤⼩端模式。
//写代码判断当前是⼤端机还是⼩端机
//UN是⼀个联合体,所有变量公⽤⼀块内存,在内存中的存储是按最⻓的那个变量所需要的位数来开辟内存的。
#include<iostream>
using namespace std;
union UN{char ch;int data;
};
int main()
{union UN un;un.data = 0x1a2b3c4d;if(un.ch == 0x4d)printf( "这是⼀个⼩端机"); //在x86平台上线读取低位再读取⾼位地址数据else if(un.ch == 0x1a)printf("这是⼀个⼤端机");elseprintf("⽆法判定该机器" );return 0;
}
#include <arpa/inet.h>
/*主机字节顺序 --> ⽹络字节顺序*/● uint32_t htonl(uint32_t hostlong); /* IP*/
● uint16_t htons(uint16_t hostshort); /* 端⼝*/in_addr_t inet_addr(const char *cp); //将⼀个点分字符串IP地址转换为⼀个32位的
⽹络序列IP地址。所属头⽂件:Winsock2.h (windows) arpa/inet.h (Linux)/*⽹络字节顺序 --> 主机字节顺序*/
● uint32_t ntohl(uint32_t netlong); /* IP*/
● char *inet_ntoa(struct in_addr in);//将⼀个32位的⽹络字节序转换为⼀个点分⼗进制字符串struct in_addr //结构体in_addr ⽤来表示⼀个32位的IPv4地址。
{in_addr_t s_addr; //in_addr_t ⼀般为 32位的unsigned int,其字节顺序为⽹络顺序
};
⼆、基于TCP/IP协议的Socket通信
2.1 基于TCP/ip的相关通信api简介
三、 TCP协议通信流程
服务器建立步骤:
1. 创建套接字
#include <sys/types.h> /* See NOTES */ //需要包含的头文件
#include <sys/socket.h>//建⽴⼀个新的socket(即为建⽴⼀个通信端⼝)int socket(int domain, int type, int protocol);成功返回⾮负的套接字描述符,失败返回 -1参数说明:domain:即协议域,⼜称为协议族(family)Name Purpose Man pageAF_UNIX, AF_LOCAL Local communication unix(7)AF_INET IPv4 Internet protocols ip(7)AF_INET6 IPv6 Internet protocols ipv6(7)AF_IPX IPX - Novell protocolsAF_NETLINK Kernel user interface device netlink(7)AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)AF_AX25 Amateur radio AX.25 protocolAF_ATMPVC Access to raw ATM PVCsAF_APPLETALK AppleTalk ddp(7)AF_PACKET Low level packet interface packet(7)AF_ALG Interface to kernel crypto APItype:SOCK_STREAM TCPSOCK_DGRAM UDPSOCK_SEQPACKET 为最⼤⻓度固定的数据报提供有序、可靠、基于双向连接的数据
传输路径:SOCK_RAW 原始套接字SOCK_RDM 提供不保证排序的可靠数据报层。protocol:⽤于指定socket所使⽤的传输协议编号,通常默认设置为0即可0选择type类型对应的默认协议;IPPROTO_TCP:TCP传输协议;IPPROTO_UDP:UDP传输协议;
2. 绑定套接字和服务器地址
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>//⽤来给参数sockfd的socket设置⼀个名称,该名称由addr参数指向的sockadr结构int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen
);返回说明:成功返回 0 失败返回 -1⽤途:主要⽤与在TCP中的连接形参说明:sockfd 套接字⽂件描述符addr 服务器地址信息struct sockaddr {sa_family_t sa_family;char sa_data[14];}但在编程中⼀般使⽤下边这种等价结构sockaddr,对于IPV4我们常⽤这个结构注意:使⽤该结构需要包含:#include <netinet/in.h>头⽂件 ****struct sockaddr_in {sa_family_t sin_family; IPV4对应AF_INET//htons()u_int16_t sin_port; 端⼝号//sin_port存储端⼝号(使⽤⽹络字节顺序)struct in_addr sin_addr; IP地址 //inet_addr()将字符串形象ip转⽹络字节序};/* Internet address. */struct in_addr {u_int32_t s_addr; IP地址};addrlen addr的⻓度 sizeof(struct sockaddr)//如果使⽤IPV6地址,需要⽤这个结构来定义变量存放ipv6相关信息 struct sockaddr_in6 {sa_family_t sin6_family; /* AF_INET6 */in_port_t sin6_port; /* port number */uint32_t sin6_flowinfo; /* IPv6 flow information */struct in6_addr sin6_addr; /* IPv6 address */uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */};struct in6_addr {unsigned char s6_addr[16]; /* IPv6 address */};
3. 监听模式
#include <sys/socket.h>//⽤于等待参数sockfd的scoket连线int listen(int sockfd, int backlog);返回值说明:成功返回0,失败返回-1sockfd 套接字⽂件描述符backlog 监听队列⻓度(等待连接的客户端的个数)缺省值20,最⼤值为128即为规定了内核应该为相应套接⼝排队的最⼤连接个数
4. 等待客户端连接的到来
#include <sys/types.h>
#include <sys/socket.h>//接收socket的连线int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen
); 返回值说明:成功返回连接的客户端的套接字⽂件描述符 失败返回 -1参数说明:sockfd 服务器套接字⽂件描述符addr 客户端信息地址,做返回值⽤的,不获取可以直接输⼊NULLaddrlen addr的⻓度,注意是⼀个指针类型 ,传⼊指定地址的⻓度,不指定则NULL
5. 读写函数
读(read/recvfrom/msgrcv): 读的本质来说其实不能是读,在实际中, 具体的接收数据不是由这些调⽤来进⾏,是由 于系统底层⾃动完成的。read 也好,recv 也好只负责把数据从底层缓冲copy 到我 们指定的位置.
写的本质也不是进⾏发送操作,⽽是把⽤户态的数据copy 到系统底层去,然后再由系 统进⾏发送操作,send,write返回成功,只表示数据已经copy 到底层缓冲,⽽不表 示数据已经发出,更不能表示对⽅端⼝已经接收到数据.
#include<unistd.h>
//将数据写⼊已打开的⽂件内,写⼊count个字节到参数fd所指的⽂件内。
ssize_t write(int fd,const void*buf,size_t count);
//从已打开的⽂件中读取数据
ssize_t read(int fd,void*buf,size_t count);
返回值:读取到的实际数据数,如果返回0表示已经到达⽂件末尾或⽆可读取的数据,当read()函数
返回值为0时,
表示对端已经关闭了 socket,这时候也要关闭这个socket,否则会导致socket泄露。
当read()或者write()函数返回值⼤于0时,表示实际从缓冲区读取或者写⼊的字节数⽬
当read()或者write()返回-1时,⼀般要判断errno
⼀般是读写操作超时了,还未返回。这个超时是指socket的SO_RCVTIMEO与SO_SNDTIMEO两个属
性。
所以在使⽤阻塞socket时,不要将超时时间设置的过⼩。不然返回了-1,
你也不知道是socket连接是真的断开了,还是正常的⽹络抖动。⼀般情况下,阻塞的socket返回了
-1,
都需要关闭重新连接。
Close()和shutdown()——结束数据传输
当所有的数据操作结束以后,你可以调⽤close()函数来释放该socket,从⽽
停⽌在该socket上的任何数据操作:close(sockfd);
6. 关闭套接字以及连接的客户端
close(关闭的东西);
客户端连接步骤:
1. 建立通讯套接字
//1.创建通讯套接字 int clifd=socket(AF_INET,SOCK_STREAM,0);
2. 客户端建⽴socket连线
//2.客户端配置要连接服务器的参数struct sockaddr_in addr; addr.sin_family=AF_INET; //IPV4addr.sin_port=htons(8000); //端口addr.sin_addr.s_addr=inet_addr("127.0.0.1"); //主机地址 --> 网络字节序int ret=connect(clifd,(struct sockaddr *)&addr,sizeof(addr)); //连接服务器if(ret==-1){printf("connect failed\n");return -1;}
3. 读写数据
//3. 读取或者向服务端发送数据write(clifd,"hello",6);
4. 关闭套接字
// 4.close clientclose(clifd);
并发服务器
TCP服务器⼀次只能接收⼀个客户端的连接的请求,只有在该客户端的所有请求都 满⾜后,服务器才可以继续响应后边的请求,如果⼀个客户端占⽤服务器不释放, 其他客户端都不能⼯作了,因此上述的TCP服务器⼜称为循环服务器,鉴于TCP循 环服务器的缺陷,很少TCP服务器采⽤。 为了解决循环TCP服务器的缺陷,⼈们⼜想出了并发服务器模型。并发服务器的思 想为:每⼀个客户端的请求并不由服务器直接处理,⽽是由服务器创建⼀个⼦进程 或⼦线程来解决。
1. 通过父子进程来接收和发送数据
//通过父子进程来接收和发送数据
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>//服务器端----负责接收客户端发送的数据
#define SER_PORT 8001
#define SER_IP "127.0.0.1"void pro_sig(int sig)
{wait(NULL);
}void talk(int fd)
{char buff[100]={0};char temp[100]={0};while(1){int ret= read(fd,buff,100); if(ret==0||strncmp(buff,"exit",4)==0){break;}printf("recv data=%s\n",buff); sprintf(temp,"%s:%s","from ser",buff);write(fd,temp,100); memset(temp,0,100);memset(buff,0,100);}close(fd);}// build server program
int main()
{
//1.create socekt nodeint serfd=socket(AF_INET,SOCK_STREAM,0);if(serfd==-1){printf("create socket failed\n");return -1;}//2.bind addr for serverstruct sockaddr_in addr;addr.sin_family=AF_INET;addr.sin_port=htons(SER_PORT);addr.sin_addr.s_addr=inet_addr(SER_IP);int ret=bind(serfd,(struct sockaddr*)&addr,sizeof(addr));
if(ret==-1){printf("bind failed\n");return -2;}//3.start listenret=listen(serfd,10);if(ret==-1){printf("listen failed\n");return -3;}//4. receive link from client computersignal(SIGCHLD,pro_sig); //父进程获取子进程结束的信号 并把子进程释放掉while(1){printf("waitting connect ......\n");struct sockaddr_in cliaddr;int len=sizeof(cliaddr);int clifd= accept(serfd,(struct sockaddr*)&cliaddr,&len);if(clifd==-1){printf("create client socket file failed\n");return -4;}printf("client ip=%s,port=%d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));pid_t pid=fork();if(pid==0){talk(clifd);exit(0);}else if(pid==-1){ exit(0);}}close(serfd);return 0;}
2. 基于多进程构建的并发服务器
//基于多进程构建的并发服务器
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>//基于多进程构建的TCP/ip并发服务器
#define MAX_NUM 3#define SER_PORT 8001
#define SER_IP "127.0.0.1"union semun
{int val;
};int semid=-1;
void create_sem()
{key_t key=ftok("/bin/mkdir",2);semid=semget(key,1,IPC_CREAT|0600);if(semid==-1){printf("create sem failed\n");exit(0);}
}void init_sem(int val)
{union semun sem_val;sem_val.val=val;int ret=semctl(semid,0,SETVAL,sem_val);if(ret==-1){printf("init sem failed\n");exit(0);}}void sem_p()
{struct sembuf value;value.sem_num=0;value.sem_op=-1;
value.sem_flg=SEM_UNDO;
int ret=semop(semid,&value,1);
if(ret==-1){printf("operator sem failed\n"); exit(0);}
}void sem_v()
{struct sembuf value;value.sem_num=0;value.sem_op=+1;
value.sem_flg=SEM_UNDO;
int ret=semop(semid,&value,1);
if(ret==-1){printf("operator sem failed\n"); exit(0);}
}void destroy_sem()
{int ret=semctl(semid,0,IPC_RMID,NULL);if(ret==-1){printf("destroy failed\n");exit(0);}
}void pro_sig(int sig)
{wait(NULL);}void talk(int fd)
{char buff[100]={0};char temp[100]={0};while(1){int ret= read(fd,buff,100); if(ret==0||strncmp(buff,"exit",4)==0){break;}printf("recv data=%s\n",buff); sprintf(temp,"%s:%s","from ser",buff);write(fd,temp,100);memset(temp,0,100);memset(buff,0,100);}close(fd);printf("find child process exit\n");sem_v();}// build server program
int main()
{create_sem();init_sem(MAX_NUM);
//1.create socekt nodeint serfd=socket(AF_INET,SOCK_STREAM,0);if(serfd==-1){printf("create socket failed\n");return -1;}//2.bind addr for serverstruct sockaddr_in addr;addr.sin_family=AF_INET;addr.sin_port=htons(SER_PORT);addr.sin_addr.s_addr=inet_addr(SER_IP);int ret=bind(serfd,(struct sockaddr*)&addr,sizeof(addr));
if(ret==-1){printf("bind failed\n");return -2;}//3.start listenret=listen(serfd,10);if(ret==-1){printf("listen failed\n");return -3;}//4. receive link from client computersignal(SIGCHLD,pro_sig);while(1){printf("waitting connect ......\n");struct sockaddr_in cliaddr;int len=sizeof(cliaddr);sem_p();int clifd= accept(serfd,(struct sockaddr*)&cliaddr,&len);if(clifd==-1){printf("create client socket file failed\n");return -4;}printf("client ip=%s,port=%d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));pid_t pid=fork();if(pid==0){printf("create child process successed\n");talk(clifd);exit(0);}else if(pid==-1){ exit(0);}}close(serfd);destroy_sem();
return 0;}
四、基于UDP/IP协议的Socket通信
4.1 基于UDP/IP通信的相关api简介
1. 发送UDP报格式数据
#include <sys/types.h>
#include <sys/socket.h>
//把UDP数据报发给指定地址
int sendto (int sockfd, const void *buf, int len, unsigned int flags,const struct sockaddr *to, int tolen);
参数说明:sockfd 套接字⽂件描述符buf 存放发送的数据len 期望发送的数据⻓度flags 0to struct sockaddr_in类型,指明UDP数据发往哪⾥报tolen: 对⽅地址⻓度,⼀般为:sizeof(struct sockaddr_in)。
2. 接收UDP报格式数据
#include <sys/types.h>
#include <sys/socket.h>
//接收UDP的数据
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,struct sockaddr *from, int *fromlen);
参数意义和sentdo类似,其中romlen传递是接收到地址的⻓度
例如 int p=sizeof(struct adrr_in),最后⼀个参数就传为&p
基于udp/ip的通信基本案例
注意:如果数据流量突然增⼤,也可以通过如下函数设置发送或接收缓冲区的⼤ ⼩, 调整UDP缓冲区⼤⼩:使⽤函数setsockopt()函数修改接收缓冲区⼤⼩ int setsockopt(int sockfd,int level,int optname,const void *optval, socklen_t optlen); level:选项定义的层次:⽀持soL_SOCKET,IPPROTO_TCP,IPPROTO_IP,和 IPPROTO_IPV6optname:
需设置得选项so_RCVBUF(接收缓冲区),So_SNDBUF(发送缓冲区)
ljs@ljs-virtual-machine:~$ cat /proc/sys/net/ipv4/tcp_rmem //覆盖 net.cor
e.rmemmax
4096 131072 6291456
读缓存最⼩值(4096)、默认值(87380)、最⼤值(6291456)(单位:字节),
ljs@ljs-virtual-machine:~$ cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
UDP接收缓冲区默认值:cat /proclsys/net/core/rmem_default
4.2 UDP⼴播
⼴播简介
从上述讲的例⼦中,不管是TCP协议还是UDP协议,都是”单播”, 就是”点对点”的进⾏通信,如果要对⽹络⾥⾯的所有主机进⾏通信,实现”点对多”的通信,我们可 以使⽤UDP中的⼴播通信。
理论上可以像播放电视节⽬⼀样在整个Internet 上发送⼴播数据,但是⼏乎没有路 由器转发⼴播数据,所以,⼴播程序只能应⽤在本地⼦⽹中。
⼴播的特点:
1. ⼴播需要有发送⽅和接收⽅,必须有⼀些线程在机器上监听到来的数据。⼴播 的缺点是如果有多个进程都发送⼴播数据,⽹络就会阻塞,⽹络性能便会受到 影响。
2. ⼴播发送不是循环给⽹络中的每⼀个IP发送数据,⽽是给⽹络中⼀个特定的IP 发送信息,这个IP就是⼴播地址,⼴播发送⽅:使⽤setsockopt打开 SO_BROADCAST, 设置⼴播地址 255.255.255.255,设置⼴播端⼝号。⼴播 接收⽅:将套接字绑定到指定的⼴播端⼝号, 监听数据到来
3. ⼴播数据发送只能采⽤UDP协议,⼴播UDP与单播UDP的区别就是IP地址不 同,⼴播使⽤⼴播地址255.255.255.255,将消息发送到在同⼀⼴播⽹络上的每个主机。
setsockopt函数:
功能是⽤来为⽹络套接字设置选项值,具体如下:#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);参数:
sock:将要被设置或者获取选项的套接字。
level:选项所在的协议层,整个⽹络协议中存在很多层,指定由哪⼀层解析;通
常是
SOL_SOCKET,也有IPPROTO_IP/IPPROTO_TCP。optname:需要操作的选项名。常⻅的⽐如SO_BROADCAST 允许发送⼴播数
据 int
optval:对于setsockopt(),指向包含新选项值的缓冲(设置的选项值);对于
getsockopt(),指向返回选项值的缓冲。
optlen:对于getsockopt(),作为⼊⼝参数时,选项值的最⼤⻓度。作为出⼝参
数时,选项值的实际⻓度。对于setsockopt(),现选项的⻓度。
若⽆错误发⽣,setsockopt()返回0。否则的话,返回SOCKET_ERROR错误,应⽤程序可通过
WSAGetLastError()获取相应错误代码。
如果希望端⼝断开后⽴即要使⽤,可以使⽤该函数的参数2设置为SO_REUSEADDR
⼀般来说,⼀个端⼝释放后会等待两分钟之后才能再被使⽤,SO_REUSEADDR
是让端⼝释放后⽴即就可以被再次使⽤。 ⽤于对TCP套接字处于TIME_WAIT状态
下的socket,才可以重复绑定使⽤
INADDR_ANY代表指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地 址”、“任意地址”;表示本地上所有的IP地址。 因为有些机⼦不⽌⼀块⽹卡,多⽹卡的情况下,这个就表示所有⽹卡ip地址的意思。
INADDR_BROADCAST选项
INADDR_BROADCAST 代表255.255.255.255的⼴播地址,⼴播消息不会在当前路由器进⾏转发,
作⽤范围只能在当前局域⽹。
当在客户端⽹络编程中,如绑定的地址是INADDR_BROADCAST表示是⼴播通信。
例子:
ljs@ljs-virtual-machine:~/0808$ cat send.c recv.c
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>//所有人发送的信息所有人都能看得到别人发的内容
//利用UDP发送广播----广播的发送端
int main()
{
//1.create socket fileint fd=socket(AF_INET,SOCK_DGRAM,0);//2.enable broadcast function for socketint a=1; int ret=setsockopt(fd,SOL_SOCKET,SO_BROADCAST,&a,sizeof(int));if(ret==-1){printf("set socket failed\n");return -1;}int count=0;char buff[20]={0};struct sockaddr_in addr;addr.sin_family=AF_INET;addr.sin_port=htons(8000);addr.sin_addr.s_addr=inet_addr("255.255.255.255"); //广播的固定地址while(1){sprintf(buff,"%s--%d","广播",++count);sendto(fd,buff,20,0,(struct sockaddr*)&addr,sizeof(addr));sleep(1);if(count==1000) break;}close(fd);return 0;
}//广播的接收端
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>int main()
{
//1.create socket fileint fd=socket(AF_INET,SOCK_DGRAM,0);//2. bind addr for reciverstruct sockaddr_in addr;addr.sin_family=AF_INET;addr.sin_port=htons(8000);addr.sin_addr.s_addr=inet_addr("255.255.255.255");bind(fd,(struct sockaddr*)&addr,sizeof(addr));
char buff[20]={0};while(1){recvfrom(fd,buff,20,0,NULL,NULL); //因为广播的地址是固定的,所以不需要去获取它的地址printf("get data=%s\n",buff);memset(buff,0,20);}close(fd);return 0;
}