Linux 网络编程
- 背景知识:
- 主机字节序列和网络字节序列
- IP地址的转换
- API
- 网络编程接口
- 网络节序与主机节序转换函数
- IP地址转换函数
- 数据读写
- TCP编程
- 编程步骤:
- 客户端链接服务端成功的条件
- 多线程实现服务端并发
- 多进程实现服务端并发
- 注意:
- UDP编程
- 编程步骤
- 实现
- netstat
- netstat -natp
- netstat -naup
背景知识:
- ipv4地址:32位 网络号+ 主机号
- ipv6地址:128位
- 端口号: short类型 应用程序的一个代号
当我们进行网络编程的时候,只需要关注于对方进程的ip地址和端口号(0-1024为知名端口,用户不能随便使用,1024-4096为保留端口,用户也不能随便使用,端口号必须4096以上才可以用)就可以了。
主机字节序列和网络字节序列
主机字节序列分为大端节序和小端节序,不同的主机采用的节序不同。
大端节序:高位字节存放在地址的低地址处。
小端节序:高位字节存放在地址的高地址处。
当两个字节节序不同的主机进行通信时,这样会产生冲突。
所以就有了网络字节序列。
网络字节序列规定使用整形数据时使用大端节序来传输。
IP地址的转换
人们习惯用点分十进制字符串表示 IPV4地址,但编程中我们需要先把它们转化。
API
网络编程接口
int socket(int domain, int type, int protocol);创建套接字
返回值:成功返回套接字的文件描述符,失败返回-1
domain:设置套接字的协议簇,AF_UNIX AF_INET AF_INET6
type:设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM
protocol:一般设置为0,表示使用默认协议
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);将 sockfd 与一个socket地址绑定。
返回值:成功返回0,失败返回-1
sockfd:是网络套接字描述符
addr:地址结构
addrlen: socket 地址的长度
int listen(int sockfd, int backlog);创建一个监听队列以存储待处理的客户连接。
返回值;成功返回0,失败返回-1
sockfd:被监听的socket套接字
backlog:表示处于完全连接状态的 socket的上限
int accept(int sockfd, struct sockaddr*addr, socklen_t *addrlen);accept()从 listen 监听队列中接收一个连接,成功返回一个新的连接socket,该socket 唯一地标识了被接收的这个连接,失败返回-1
sockfd:是执行过 listen系统调用的监听socket
addr:用来获取被接受连接的远端socket地址
addrlen:该socket地址的长度
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);客户端需要通过此系统调用来主动与服务器建立连接,
返回值:成功返回0,失败返回-1
sockfd:由socket返回的一个socket。
serv_addr:服务器监听的socket地址
addrlen:这个地址的长度
int close(int sockfd);关闭一个连接,实际上就是关闭该连接对应的socket。
网络节序与主机节序转换函数
#include<netinet/in.h>
uint32_t htonl(uint32_t hostlong);长整型的主机字节序转网络字节序
uint32_t ntohl(uint32_t netlong);长整型的网络字节序转主机字节序
uint16_t htons(uint16_t hostshort);短整形的主机字节序转网络字节序
uint16_t ntohs(uint16_t netshort);短整型的网络字节序转主机字节序
IP地址转换函数
#include <arpa/inct.h>
in_addr_t inet_addr(const char *cp);字符串表示的IPV4地址转化为网络字节序
char* inet_ntoa(struct in_addr in);/IPV4地址的网络字节序转化为字符串
数据读写
TCP数据读写
ssize_t recv(int sockfd, void *buff, size_t len, int flags);读取sockfd上的数据,buff和 len参数分别指定读缓冲区的位置和大小
ssize_t scnd(int sockfd, const void *buff, size_t len, int flags);往socket上写入数据,buff和len参数分别指定写缓冲区的位置和数据长度flags参数为数据收发提供了额外的控制
UDP数据读写
ssize_t recvfrom(int sockfd, void * buff, size_t len, int flags,struct sockaddr* src_addr, socklen_t * addrlcn);读取 sockfd上的数据,buff和len参数分别指定读缓冲区的位置和大小,src_addr记录发送端的socket地址addrlen指定该地址的长度
ssize_t sendto(int sockfd, void * buff, size_t len, int flags,struct sockaddr * dest_addr, socklen_t addrlcn);往socket上写入数据,buff和 len参数分别指定写缓冲区的位置和数据长度dest_addr指定接收数据端的 socket地址addrlen指定该地址的长度
TCP编程
编程步骤:
1.服务端
1. socket()
2. bind()
3. listen()backlog的最大值不能超过128,它的值加1则是全连接队列的大小。非阻塞函数
4. accept()是不是阻塞取决于socket是阻塞还是非阻塞模式,一般默认创建的是阻塞模式。
5. recv() 是不是阻塞取决于socket是阻塞还是非阻塞模式,一般默认创建的是阻塞模式。
6. send()是不是阻塞取决于socket是阻塞还是非阻塞模式,一般默认创建的是阻塞模式。
7. close()
2.客户端
- socket()
- connect()
- send()
- recv()
- close()
我们先来看一下三次握手的状态图。首先当客户端和服务端进行连接时,它是一种过程,中间有很多状态。
Linux内核会为维护两个队列
-
为SYN_RCVD状态的socket维护一个队列(半连接队列)
等待收到对方的ACK,接着把该socket放到全连接队列中
-
为处于ESTABLISTENsocket维护一个队列(全连接队列)
等待服务端进行accept的时候从这个队列中取出socket。
listen函数的backlog的值加1则是这个全连接队列的大小。
对于半连接队列的大小,我们可以通过下面命令来查看,它同样可以修改。
因为队列在内核中维护,这个也需要消耗大量资源,所以backlog的大小不能太大,不能超过128。
所以说listen中backlog的值并不是服务端能连接客户端的大小。
客户端链接服务端成功的条件
1.端口,ip地址,服务类型都正确
2.服务器正在运行
3.网络正常
4.服务器资源充足
多线程实现服务端并发
服务端
threadser.cpp
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>
#include<bits/socket.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<pthread.h>
int sock_init()
{
int socketfd = socket(AF_INET,SOCK_STREAM,0);
if(socketfd == -1)
{
std::cout<<"socket erro"<<std::endl;
exit(1);
}
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
int res = bind(socketfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(res == -1)
{
std::cout<<"bind erro "<<std::endl;
exit(1);
}
res = listen(socketfd,5);
if(res == -1)
{
std::cout<<"listen erro"<<std::endl;
exit(1);
}
return socketfd;
}
void * fun(void *);
int main()
{
int socketfd = sock_init();
while(1)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(socketfd,(struct sockaddr *)&caddr,(socklen_t *)&len);
if(c<=0)
{
std::cout<<"accept erro"<<std::endl;
break;
}
std::cout<<"c= "<<c<<"ip= "<<inet_ntoa(caddr.sin_addr)<<"port= "<<ntohs(caddr.sin_port)<<std::endl;;
pthread_t id;
pthread_create(&id,NULL,fun,(void *)(long)c);
}
}
void * fun(void * arg)
{
int c = (int)(long)arg;
while(1)
{
char buff[128]={0};
int n = recv(c,buff,1,0);
if(n<=0)
{
break;
}
std::cout<<"recive ="<<buff<<"len="<<n<<std::endl;
send(c,"ok",2,0);
}
std::cout<<c<<"close"<<std::endl;
close(c);
return NULL;
}
客户端
cli.cpp
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>
#include<bits/socket.h>
#include<arpa/inet.h>
#include<sys/socket.h>
int main()
{
int socketfd = socket(AF_INET,SOCK_STREAM,0);
if(socketfd == -1)
{
std::cout<<"socket erro"<<std::endl;
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port =htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(socketfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(res == -1)
{
std::cout<<"connect erro"<<std::endl;
exit(1);
}
while(1)
{
char buff[128]={0};
std::cout<<"please input"<<std::endl;
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
send(socketfd,buff,strlen(buff)-1,0);
memset(buff,0,sizeof(buff));
recv(socketfd,buff,127,0);
std::cout<<"buff = "<<buff<<std::endl;
}
close(socketfd);
exit(0);
}
运行结果
客户端1
客户端2
服务端
多进程实现服务端并发
服务端
courseser.cpp
#include<iostream>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>
#include<bits/socket.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<pthread.h>
#include<signal.h>
void fun(int sig)
{
wait(NULL);
}
int sock_init()
{
int socketfd = socket(AF_INET,SOCK_STREAM,0);
if(socketfd == -1)
{
std::cout<<"socket erro"<<std::endl;
exit(1);
}
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
int res = bind(socketfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(res == -1)
{
std::cout<<"bind erro "<<std::endl;
exit(1);
}
res = listen(socketfd,5);
if(res == -1)
{
std::cout<<"listen erro"<<std::endl;
exit(1);
}
return socketfd;
}
int main()
{
signal(SIGCHLD,fun);
int socketfd = sock_init();
while(1)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(socketfd,(struct sockaddr *)&caddr,(socklen_t*)&len);
if(c<=0)
{
std::cout<<"accept erro"<<std::endl;
break;
}
std::cout<<"c="<<c<<" ip="<<inet_ntoa(caddr.sin_addr)<<" port="<<ntohs(caddr.sin_port)<<std::endl;;
pid_t pid = fork();
if(pid ==0)
{
while(1)
{
char buff[128]={0};
int n = recv(c,buff,127,0);
if(n<=0)
{
break;
}
std::cout<<"recive ="<<buff<<" len="<<n<<std::endl;
send(c,"ok",2,0);
}
std::cout<<c<<"close"<<std::endl;
close(c);
}
}
exit(0);
}
客户端
cli.cpp
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>
#include<bits/socket.h>
#include<arpa/inet.h>
#include<sys/socket.h>
int main()
{
int socketfd = socket(AF_INET,SOCK_STREAM,0);
if(socketfd == -1)
{
std::cout<<"socket erro"<<std::endl;
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port =htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(socketfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(res == -1)
{
std::cout<<"connect erro"<<std::endl;
exit(1);
}
while(1)
{
char buff[128]={0};
std::cout<<"please input"<<std::endl;
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
send(socketfd,buff,strlen(buff)-1,0);
memset(buff,0,sizeof(buff));
recv(socketfd,buff,127,0);
std::cout<<"buff = "<<buff<<std::endl;
}
close(socketfd);
exit(0);
}
运行结果
服务端
客户端1
客户端2
注意:
关闭的时候要先关闭所以客户端,然后再关闭服务端,要不然运行服务端的时候会出现端口占用的情况,无法启动。
UDP编程
与TCP编程相比,UDP编程一定要每次都把数据读完,因为它是无连接的,如果没有读完的话,则会把剩余的数据丢掉。
udp在局域网中很难丢数据
编程步骤
服务端
1.socket()
2.bind()
3.recvfrom()
4.sendto()
5.close()
客户端
1.socket()
//bind()可有可无
2.sendto()
3.recvfrom()
4.close()
实现
ser.cpp
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
std::cout<<"sockfd erro"<<std::endl;
exit(1);
}
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int res =bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(res == -1)
{
std::cout<<"bind erro"<<std::endl;
exit(1);
}
while(1)
{
char buff[128]={0};
int len = sizeof(caddr);
int num = recvfrom(sockfd,buff,127,0,(struct sockaddr *)&caddr,(socklen_t*)&len);
std::cout<<"datalength="<<num<<" ip="<<inet_ntoa(caddr.sin_addr)<<" buff="<<buff<<std::endl;
sendto(sockfd,"ok",2,0,(struct sockaddr *)&caddr,sizeof(caddr));
}
close(sockfd);
exit(0);
}
cli.cpp
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
std::cout<<"sockfd erro"<<std::endl;;
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
while(1)
{
std::cout<<"input"<<std::endl;
char buff[128]={0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3) == 0)
{
break;
}
sendto(sockfd,buff,strlen(buff)-1,0,(struct sockaddr *)&saddr,sizeof(saddr));
memset(buff,0,sizeof(buff));
int len = sizeof(saddr);
recvfrom(sockfd,buff,127,0,(struct sockaddr *)&saddr,(socklen_t*)&len);
std::cout<<"recive ="<<buff<<std::endl;
}
close(sockfd);
exit(0);
}
运行结果
服务端
客户端1
客户端2
对于UDP编程来说,它是无连接的,发送数据的时候只需要知道对方的IP和端口就可以了
TCP 可靠,开销大
UDP 开销小,不可靠
UDP和TCP可以同时复用一个端口
一个进程中可以创建多个套接字,只要使用的端口不同就可以了
netstat
netstat -natp
查看tcp连接的各种信息