前言
当我们进行网络编程的时候,只需要关注于对方进程的ip地址和端口号(0-1024为知名端口,用户不能随便使用,1024-4096为保留端口,用户也不能随便使用,端口号必须4096以上才可以用)就可以了。
API接口函数
网络编程接口
所需头文件: #include<sys/socket.h> //接口函数
int socket(int domain, int type, int protocol) :创建套接字
domain:设置套接字的协议簇,AF_UNIX AF_INET AF_INET6
type:设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM
protocol:一般设置为0,表示使用默认协议
返回值:成功返回套接字的文件描述符,失败返回-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):将 sockfd 与一个socket地址绑定。
sockfd:是网络套接字描述符
addr:地址结构
addrlen:socket 地址的长度
返回值:成功返回0,失败返回-1
int listen(int sockfd, int backlog):创建一个监听队列以存储待处理的客户连接。
sockfd:被监听的socket套接字 backlog:表示处于完全连接状态的 socket的上限(一般写5)
返回值:成功返回0,失败返回-1
int accept(int sockfd, struct sockaddr*addr, socklen_t *addrlen):accept()从 listen 监听队列中接收一个连接。
sockfd:是执行过 listen系统调用的监听socket
addr:用来获取被接受连接的远端socket地址
addrlen:该socket地址的长度
返回值:成功返回一个新的连接socket,该socket 唯一地标识了被接收的这个连接,失败返回-1
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen):客户端需要通过此系统调用来主动与服务器建立连接,
sockfd:由socket返回的一个socket。
serv_addr:服务器监听的socket地址
addrlen:这个地址的长度
返回值:成功返回0,失败返回-1
int close(int sockfd):关闭一个连接,实际上就是关闭该连接对应的socket。
sockfd:由socket返回的一个socket。
主机字节序列转换网络字节序列接口
所需头文件: #include<netinet/in.h>
uint32_t htonl( uint32_t hostlong ):长整型的 主机字节序列转换为长整型的 网络字节序列
hostlong:长整型的主机字节序列
uint32_t ntohl( uint32_t netlong ):长整型的 网络字节序列转换为长整型的 主机字节序列
netlong:长整型的网络字节序列
uint16_t htons( uint16_t hostshort ):短整型的 主机字节序列转换为短整型的 网络字节序列
hostshort:短整型的主机字节序列
uint16_t ntohs( uint16_t netshort ):短整型的 网络字节序列转换为短整型的 主机字节序列
netshort:短整型的网络字节序列
ip地址转换接口
所需头文件:#include<arpa/inet.h>
in_addr_t inet_addr( const char* cp ):字符串表示的ipv4地址转换位为网络字节序列
cp:字符串表示的ipv4地址(例如:192.168.0.1)
char* inet_ntoa( struct in_addr in ):ipv4地址的网络字节序列转换为字符串表示
in:存放数据为32位无符号整形的结构体变量
数据接收发送接口
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参数为数据收发提供了额外的控制
TCP编程
编程流程:
服务器端:
1. socket()
2. bind()
3. listen():backlog的最大值不能超过128,它的值加1则是全连接队列的大小。
4. accept()
5. recv()
6. send()
7. close()
客户端:
1.socket()
2.connect() //发起连接,开始三次握手
3.send()
4.recv()
5.close()
监听队列及三次握手的建立过程
三次握手
首先了解一下TCP报头
四次挥手
三次握手四次挥手相关问题
TIME_WAIT状态的意义?
答:
①确保可靠的终止TCP。当服务器发送FIN报文给客户端时,客户端回复ack确认报文在传输途中丢失,服务器就会再次发送FIN报文直到接收到ack确认报文终止TCP连接。
②确保迟到的TCP报文段被识别并丢弃。当客户端给服务器发送了一段消息,但此时客户端突然结束,如果不清理这段报文,当我们再次用相同的ip和端口打开客户端连接服务器时,我们再发消息,就会发现消息不是这次发送的,因此需要TIME_WAIT等待一段时间去处理迟来的报文。
为什么断开连接等待时间是2MSL?
(MSL:报文在网络中最长的存活时间)
答:
因为网络有不确定因素所以有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文,也就是2MS,2MSL就是一个发送和一个回复所需的最大时间。如果客户端ACK丢失,导致服务器没有收到ACK,那么服务端将不断重复发送FIN片段。所以客户端不能立即关闭,它必须确认服务端已经接收到了该ACK。客户端会在发送出确认ACK之后进入到TIME_WAIT状态,它会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。如果直到2MSL,客户端都没有再次收到FIN,那么客户端推断ACK已经被成功接收,则结束TCP连接。
客户端链接服务端成功的条件
1.端口,ip地址,服务类型都正确
2.服务器正在运行
3.网络正常
4.服务器资源充足
多线程实现服务端并发
服务器端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
void* work_thread(void* arg) //线程函数
{
int c = (int)arg; //套接字文件描述符
while(1)
{
char buff[128] = {0};
int n = recv(c,buff,127,0); //接收客户端的消息
if(n <= 0) //没有收到消息退出
{
printf("client %d over\n",c);
break;
}
printf("buff:%s\n",buff);
send(c,"OK",2,0); //接收并打印后回复客户端OK
}
close(c); //关闭套接字文件描述符
}
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0); //创建套接字
assert(sockfd != -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(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); //绑定套接字
assert(res != -1);
listen(sockfd,5); //创建监听队列并放入
while(1)
{
int len = sizeof(saddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len); //接收连接,返回新的套接字
if(c <= 0)
{
printf("accept err\n");
break;
}
printf("c = %d ip = %s port = %d accepted\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
pthread_t id;
pthread_create(&id,NULL,work_thread,(void*)c); //创建线程
}
}
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0); //定义套接字
assert(sockfd != -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(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); //发起连接,开始三次握手
assert(res != -1);
while(1)
{
printf("input: ");
char buff[128] = {0};
fgets(buff,128,stdin);
send(sockfd,buff,strlen(buff)-1,0); //向服务器发送数据
if(strncmp(buff,"end",3) == 0)
{
break;
}
memset(buff,0,128);
recv(sockfd,buff,127,0); //接收服务其的消息
printf("read: %s\n",buff);
}
close(sockfd); //关闭套接字文件描述符
}
结果演示:
多进程实现服务端并发
服务器端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/wait.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0); //创建套接字
assert(sockfd != -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(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定套接字
assert(res != -1);
listen(sockfd,5); //创建监听队列并放入
while(1)
{
int len = sizeof(saddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len); //接收连接,返回新的套接字
if(c <= 0)
{
printf("accept err\n");
break;
}
printf("c = %d ip = %s port = %d accepted\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
pid_t id = fork(); //进程复制
if(id == 0) //子进程
{
while(1)
{
char buff[128] = {0};
int n = recv(c,buff,127,0); //接收客户端消息
if(n <= 0)
{
printf("client %d over\n",c);
break;
}
printf("buff:%s\n",buff);
send(c,"OK",2,0); //回复OK
}
}
close(c);
}
}
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -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(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); //发起连接,开始三次握手
assert(res != -1);
while(1)
{
printf("input: ");
char buff[128] = {0};
fgets(buff,127,stdin);
send(sockfd,buff,strlen(buff)-1,0);
if(strncmp(buff,"end",3) == 0)
{
break;
}
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("read: %s\n",buff);
}
close(sockfd);
}
结果演示:
send/recv缓冲区
TCP流式服务
TCP粘包问题
因为TCP流式服务的特点,导致会发生粘包问题(多次发送的数据会被一次收到)
例如我们连续发送:长 4宽 2高 5,我们将数据放入发送缓冲区中,再发送到接收缓冲区中,但我们直接读出425作为长,此时服务器会阻塞,等待我们发送宽和高。
如何解决TCP粘包问题:
①我们采用发送-回应的方式,我们发一次send,recv一次回复再去send
②定义发送接收的形式,例如[4][2][5],将'['视作数据头部,将']'视作数据结束尾部
TCP特点总结
TCP是一个可靠的,面向连接,流式服务的协议
如何保证可靠:
应答确认(客户端发送消息,服务器回复ack,客户端收到后再回复ack)
超时重传(客户端发送消息后,等待服务器回复ack,但一段时间没有收到ack时,会再重发数据,直到收到ack)
乱序重排(客户端多次发送消息,有的先被接收有的后被接收到,但是在TCP报文中每次数据有序号,它会根据序号重排数据再放入接收缓冲区)
netstat查看TCP连接状态
指令netstat -natp
可以看到TCP连接的ip,端口port,状态等
UDP编程
编程流程:
代码实现
服务器端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
assert(sockfd != -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(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
while(1)
{
char buff[128] = {0};
int len = sizeof(caddr);
recvfrom(sockfd,buff,127,0,(struct sockaddr*)&caddr,&len);
printf("ip = %s port = %d buff = %s\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port),buff);
sendto(sockfd,"OK",2,0,(struct sockaddr*)&caddr,sizeof(caddr));
}
}
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
assert(sockfd != -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)
{
printf("input:\n");
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,128);
int len = sizeof(saddr);
recvfrom(sockfd,buff,127,0,(struct sockaddr*)&saddr,&len);
printf("buff:%s\n",buff);
}
close(sockfd);
exit(0);
}
结果演示:
UDP数据报服务
并不会像TCP出现粘包问题
UDP特点总结
UDP是一个无连接,不可靠,数据报服务的协议
TCP/UDP各自使用场景
当我们需要传文件时,当一个字节丢失都会导致文件打不开,因此我们选择TCP协议
当我们需要实时通讯时,这类如微信语音聊天,或视频聊天的应用,发送方和接收方需要实时交互,也就是不允许较长延迟,即便有几句话因为网络堵塞没听清也不用等待这部分重新发送。因此选用UDP协议