一、网络编程
套接字:传输层的文件描述符
1.1TCP的C/S实现
1.1.1TCP服务器实现过程
创建套接字socket函数
调用bind函数给套接字绑定IP地址和端口号
将套接字文件描述符从主动变为被动文件,调用listen函数开始监听
通过调用accept函数被动监听客户的链接并响应
服务器调用read(recv)和write(send),收发数据,实现与客户端的通信
调用close或者shutdown关闭TCP的链接
(1)创建套接字socket函数
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型: int socket(int domain, int type, int protocol);
功能: 创建套接字,返回一个文件描述符
参数:
domian:通信域
AF_UNIX, AF_LOCAL Local communication unix(7) //本地通信
AF_INET IPv4 Internet protocols ip(7) //ipv4网络协议
AF_INET6 IPv6 Internet protocols ipv6(7) //ipv6网络协议
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK AppleTalk ddp(7)
AF_PACKET Low level packet interface packet(7) //底层协议通信
AF_ALG Interface to kernel crypto API
type:套接字类型
SOCK_STREAM : 流式套接字 ------>tcp
SOCK_DGRAM : 数据包套接字 ------>udp
SOCK_RAW : 原始套接字
protocol:附加协议,传0便是不需要其它协议
返回值:
成功:返回套接字文件描述符(sockfd)
失败:返回-1,errno被设置
(2)调用bind函数给套接字绑定IP地址和端口号
头文件: #include <sys/types.h>
#include <sys/socket.h>
原型: int bind(int socked, const struct sockaddr *addr, socklen_t addrlen);
功能: 将套接字与网络信息结构体绑定
参数:
sockfd:文件描述符,socket的返回值
addr:保存IP和端口号的网络信息结构体
通用结构体:结构体的长度,一般不用
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
网路信息结构体:
struct sockaddr_in{
sa_family_t sin_family; //地址族:AF_INET
in_port_t sin_port; //网络字节序的端口号
struct in_addr sin_addr //ip地址
struct in_addr
{
uint32_t s_addr; //网络字节序的无符号4字节整数ip地址
}
}
addrlen:addr的大小
返回值:
成功:返回0
失败:返回-1,errno被设置
出现无法绑定的情况
client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟;
解决方法:setsockopt函数
在server代码的socket()和bind()调用之间插入如下代码:
int opt = 1;
setsocket(socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
(3)将套接字文件描述符,从主动变为被动文件描述符(做监听准备):listen
头文件: #include <sys/types.h>
#include <sys/socket.h>
原型:int listen(int socked, int backlog);
功能:将套接字设置为被动监听状态
参数:
sockfd:文件描述符,socket的返回值
backlog:允许同时连接的客户端的个数,一般设置为5,10,一般小于30
返回值:
成功:0
失败:-1,errno被设置
(4)被动监听客户的连接并响应:accept函数
头文件: #include <sys/types.h>
#include <sys/socket.h>
原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:被动监听客户发起三次握手的链接请求,三次握手成功,阻塞等待客户端的连接
参数:
sockfd:文件描述符,socket的返回值
addr:被填充的网络信息结构体,如果由客户端连接服务器,服务器可以通过这个参数获取客户端的信息。
addrlen:addr的大小
返回值:
成功:返回一个通信描述符,专门用于该连接成功的客户的通信,总之后续服务器与该客户间正式通信,使用的就是accept返回的“通信描述符”来实现的
失败:-1,errno被设置
(5)服务器调用read(recv)和write(send),收发数据,实现与客户的通信
send函数
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型:size_t send(int socked, const void *buf, size_t len, int flags);
功能:向对方发送数据
参数:
参数1:用于通信的通信描述符
参数2:应用缓存,用于存放要发送的数据
参数3:buf缓存的大小
参数4:
设置为0,send阻塞发送的数据
设置MSG_NOSIGNAL:send数据时,若对方关闭链接调用send的进程便发送SIGPIEIE信号
设置MSG_DONTWAIT:非阻塞发送
设置MSG_OOB:表示发送的是带外数据
返回值:
成功:返回发送的字节数
失败:-1
recv函数
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型:size_t recv(int sockfd, void *buf, size_t len, int flags);
功能:接收对方发送的数据
参数:
参数1:通信文件描述符
参数2:应用缓存,用于存放接收的数据
参数3:buf缓存的大小
参数4:
设置为0,recv阻塞接收
设置MSG_DONTWAIT:非阻塞接收
设置MSG_OOB:表示接收的是带外数据
返回值:
成功:返回文件描述符
失败:-1
(6)调用close或者shutdown关闭TCP的链接
close:
缺点1:会一次性将读写都关掉了
缺点2:如果多个文件描述符指向了同一个链接时,如果只close关闭其中某个文件描述符时,只要其它的fd还打开着,那么链接不会被断开,直到所有的描述符都被close后才断开链接
出现多个描述指向同一个链接的原因可能两个:
1.通过dup方式复制出其它描述符
2.子进程继承了这个描述符,所以子进程的描述符也指向了链接
shutdown
头文件:#include <sys/socket.h>
原型:int shutdown(int socked,int how);
功能:可以按照要求关闭链接,而且不管有多少个描述符指向同一个链接,只要调用shutdown去操作了其中某个描述符,链接就会立即断开
参数:
参数1:TCP服务器断开连接时,使用的是accept所返回的文件描述符
参数2:如何断开链接
SHUT_RD:只断开读链接
SHUT_WR:只断开写链接
SHUT_RDWR:读写链接都断开
返回值:
成功:0
失败:-1
1.1.2 TCP客户端的实现过程
使用socket创建套接字文件,指定TCP文件
调用connect函数自动向服务器发起上次握手,进行链接
调用read/recv和write/send,来进行收发数据
调用close/shutdown关闭TCP的链接
(1)用socket创建套接字文件,指定使用TCP协议
(2)调用connect主动向服务器发起三次握手,进行连接
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:向服务器主动发起连接请求
参数:
参数1:socket所返回的套接字文件描述符
参数2:用于设置你所要连接服务器的IP和端口
参数3:参数2所指定的结构体变量的大小
返回值:
成功:0
失败:-1
(3)调用read/recv和write/send,来进行收发数据
send函数
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型:size_t send(int sockfd, const void *buf, size_t len, int flags);
功能:向对方发送数据
参数:
参数1:用于通信的通信描述符
参数2:应用缓存,用于存放你要发送的数据
参数3:buf缓存的大小
参数4:
设置为0,send阻塞发送
设置MSG_NOSIGNAL:send数据时,若对方关闭链接调用send的进程便发送SIGPIEIE信号
设置MSG_DONTWAIT:非阻塞发送
设置MSG_OOB:表示发送的是带外数据
返回值:
成功:返回发送的字节数
失败:-1
recv函数
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型:size_t recv(int sockfd, void *buf, size_t len, int flags);
功能:接收对方发送的数据
参数:
参数1:通信文件描述符
参数2:应用缓存,用于存放接收的数据
参数3:buf缓存的大小
参数4:
设置为0,recv阻塞接收
设置MSG_DONTWAIT:非阻塞接收
设置MSG_OOB:表示接收的是带外数据
返回值:
成功:返回文件描述符
失败:-1
(4)调用close或者shutdown关闭链接
close:
缺点1:会一次性将读写都关掉
缺点2:如果多个文件描述符指向同一个链接时,如果只close关闭其中某个文件描述符时,只要其它的fd还打开着,那么链接就不会断开,直到所有的描述符都被close后才断开链接。
出现多个描述符指向同一个链接的原因可能有两个:
1.通过dup方式复制出其它描述符
2.子进程继承了这个描述符,所以子进程的描述符也指向了链接
shutdown
头文件:#include <sys/socket.h>
原型:int shutdown(int sockfd, int how);
功能:可以按照要求关闭连接,而且不管有多少个描述符指向同一个连接,只要调用shutdown去操作了其中某个描述符,连接就会被立即断开
参数:
参数1:TCP服务器断开连接时,使用的是accept所返回的文件描述符
参数2:如何断开链接
SHUT_RD:只断开读连接
SHUT_WR:只断开写连接
SHUT_RDWR:读、写连接都断开
返回值:
成功:返回0
失败:-1
1.1.3 TCP代码实现
TCP服务器
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include<arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int main(int argc, char const *argv[])
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0); //IPV4协议,流式套接字,具体的协议类型
if(-1 == sockfd)
{
perror("socket");
return -1;
}
struct sockaddr_in server_addr; //保存服务器的信息
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8000);
server_addr.sin_addr.s_addr = inet_addr("192.168.98.147");
//绑定信息
int ret = bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
if(-1 == ret)
{
perror("bind");
return -1;
}
//设置监听队列
ret = listen(sockfd,10);
if(-1 == ret)
{
perror("listen");
return -1;
}
printf("等待客户端进行连接...、\n");
struct sockaddr_in Client_addr; //用于保存客户端的信息
int length = sizeof(Client_addr);
int fd = accept(sockfd,(struct sockaddr *)&Client_addr,&length);
if(-1 == fd)
{
perror("accept");
return -1;
}
printf("接收客户端的连接 %d\n",fd);
char buf[32] = {0};
while(1)
{
ret = recv(fd,buf,sizeof(buf),0);
if(-1 == ret)
{
perror("recv");
return -1;
}
if(strcmp(buf,"bye") == 0)
{
break;
}
printf("%s\n",buf);
memset(buf,0 ,sizeof(buf));
}
close(fd);
close(sockfd);
return 0;
}
TCP客户端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include<arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket");
return -1;
}
//向服务器发起连接
struct sockaddr_in server_addr; //保存服务器的信息
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8000);
server_addr.sin_addr.s_addr = inet_addr("192.168.98.147");
int ret = connect(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
if(-1 == ret)
{
perror("connect");
return -1;
}
char buf[32] = {0};
while(1)
{
scanf("%s",buf);
ret = send(sockfd,buf,strlen(buf),0);
if(-1 == ret)
{
perror("send");
return -1;
}
if(strcmp(buf,"bye") == 0)
{
break;
}
memset(buf,0,sizeof(buf));
}
close(sockfd);
return 0;
}
2.1.4TCP并发多任务服务器
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include<arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <pthread.h>
void * ClientHandler(void *arg)
{
int ret;
int fd = *(int *)arg;
char buf[32] = {0};
pthread_detach(pthread_self()); //线程结束,自动释放资源
while(1)
{
ret = recv(fd,buf,sizeof(buf),0);
if(-1 == ret)
{
perror("recv");
return (void *)-1;
}
else if(0 == ret)
{
break; //客户端异常退出
}
if(strcmp(buf,"bye") == 0)
{
break;
}
printf("接收%d客户端 %s\n",fd,buf);
memset(buf,0 ,sizeof(buf));
}
printf("%d 客户端退出!\n",fd);
close(fd);
}
int main(int argc, char const *argv[])
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0); //IPV4协议,流式套接字,具体的协议类型
if(-1 == sockfd)
{
perror("socket");
return -1;
}
struct sockaddr_in server_addr; //保存服务器的信息
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8000);
server_addr.sin_addr.s_addr = inet_addr("192.168.98.147");
//绑定信息
int ret = bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
if(-1 == ret)
{
perror("bind");
return -1;
}
//设置监听队列
ret = listen(sockfd,10);
if(-1 == ret)
{
perror("listen");
return -1;
}
printf("等待客户端进行连接...、\n");
struct sockaddr_in Client_addr; //用于保存客户端的信息
int length = sizeof(Client_addr);
while(1)
{
int fd = accept(sockfd,(struct sockaddr *)&Client_addr,&length);
if(-1 == fd)
{
perror("accept");
return -1;
}
printf("接收客户端的连接 %d\n",fd);
//为每一个客户端创建新的线程
pthread_t tid;
ret = pthread_create(&tid,NULL,ClientHandler,&fd);
if(ret != 0)
{
perror("pthread_create");
return -1;
}
}
close(sockfd);
return 0;
}
1.2 UDP的C/S实现
1.2.1 特点:
UDP协议没有建立连接特性,所以UDP协议没有自动记录对方IP和端口的特点,每次发送数据时,必须亲自指定对方的IP和端口,只有这样才能将数据发送给对方
1.2.2 UDP通信过程
调用socket创建套接字文件
bind绑定固定的ip和端口
调用sendto和recvfrom函数,发送和接收数据
sendto
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型:size_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
功能:当后两个参数为NULL和0时,功能等价于send,send专门用于TCP这种面向连接的通信,但对于像UDP这种非连接的通信,必须使用sendto,因为此时必须使用最后两个参数
参数:
参数1:socket返回的套接字描述符
对于UDP来说,套接字描述符直接用于通信
参数2:存放数据的应用缓存
参数3:应用缓存的大小
参数4:一般写0,表示阻塞发送数据
参数5:写目标ip和端口
参数6:dest_addr的大小
返回值:
成功:返回发送的字节数
失败:-1
recvfrom
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型:size_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr*src_addr, socklen_t *addrlen);
功能:收数据,最后两个参数与NULL和NULL时,功能与recv功能相同UDP通信时,常使用的是recvfrom函数,因为需要用到后两个参数
参数:
参数1:socket返回的套接字文件描述符
参数2:应用缓存,用于存放接收到的数据
参数3:buf的大小
参数4:一般写0,表示阻塞接收数据
参数5:用于保存“数据发送方”的ip和端口
返回值:
成功:返回接收到的字节数
失败:-1
1.2.3广播
一个人发,然后其它所有人都接收,则就是广播
广播只能在局域网内部有效,广播数据是无法越过路由器的,也就是路由器就是广播数据的边界
实现方法:IP地址协会曾广播地址; 例如:192.168.1.255
接收端的IP地址不能设置为固定IP,要指定为INADDR_ANY
1.2.4组播
广播是给其它所有计算机广播数据,而组播只对其它所有计算机中的某部分计算机广播数据
1.2.5UDP网络编程代码
UDP服务器
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n",__FILE__,__func__,__LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
int sockfd;
//第一步:创建套接字
if(-1 == (sockfd = socket(AF_INET,SOCK_DGRAM,0)))
{
ERRLOG("socket error");
}
struct sockaddr_in serveraddr,clientaddr;
socklen_t addrlen = sizeof(serveraddr);
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr("192.168.98.147");
serveraddr.sin_port = 8888;
//第三步:将套接字与服务器网路信息结构体绑定
int ret = bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
if(ret == -1)
{
ERRLOG("bind");
}
//进行通信
char buf[32] = {0};
while(1)
{
NEXT:
if(recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&clientaddr,&addrlen) == -1)
{
ERRLOG("recvfrom");
}
if(strcmp(buf,"bye") == 0)
{
printf("客户端%s-%d退出了\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
goto NEXT;
}
printf("%s-%d: %s\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port),buf);
}
return 0;
}
UDP客户端
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n",__FILE__,__func__,__LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
int sockfd;
//第一步:创建套接字
if(-1 == (sockfd = socket(AF_INET,SOCK_DGRAM,0)))
{
ERRLOG("socket error");
}
struct sockaddr_in serveraddr,clientaddr;
socklen_t addrlen = sizeof(serveraddr);
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr("192.168.98.147");
serveraddr.sin_port = 8888;
//进行通信
char buf[32] = {0};
while(1)
{
scanf("%s",buf);
if(sendto(sockfd,buf,32,0,(struct sockaddr *)&serveraddr,addrlen) == -1)
{
ERRLOG("sendto");
}
if(strcmp(buf,"bye") == 0)
{
printf("客户端退出了\n");
}
memset(buf,0,sizeof(buf));
}
return 0;
}