Socket编程(TCP/UDP详解)
前言:之前因为做项目和找实习没得空,计算机网络模块并没有写成博客,最近得闲了,把计算机网络模块博客补上。
目录
一,UDP编程
1)创建套接字
2)绑定端口号
3)发送与接收数据
4)UDP简单的发送数据和接收数据服务器
二,TCP编程
1)创建套接字
2)绑定端口号
3)使套接字进入监听状态
4)获取成功建立连接的的文件描述符和主机信息
5)发送与接收数据
6)连接其他主机
7)TCP简单的发送数据和接收数据服务器
scoket编程即套接字编程,是网络编程的基础,它允许两台或者多台计算机进行网络通信,这篇文章主要讲socket编程利用里面的TCP和UDP相关接口实现网络通信。
一,UDP编程
在udp编程里面,我们首先要创建一个套接字,也就是文件描述符。用来接收数据与发送数据,但注意,UDP为每一个套接字维护一个缓冲区,但是发送缓冲区是临时的、不可见的。这是为什么呢?UDP是面向无连接的,每次发送数据都是相对独立的,这允许我们可以使用临时的缓冲区,UCP数据发送完就不管任何事了,不会像TCP一样要确认对方收到,没收到还要进行重传等操作。不维护一个长久的缓冲区,也可以节省空间资源,使UDP变得轻量与高效。如果接收缓冲区设置成临时的那么数据到达后,如果应用程序没有及时读取可能出现丢失,那么如果一直等到读取完再销毁,一个套接字缓冲区可能接收很多主机的信息,可能接收缓冲区会频繁的创建,销毁,这会有很多不必要的开销。
1)创建套接字
第一个参数是网络通信协议,如IPV4或者IPV6等,具体参考下图
第二个参数是套接字的类型,使用什么方式通信,如数据报(UDP)或者字节流(TCP)等
返回值为-1代表创建失败,并设置错误码,大于0代表成功创建。
使用例子:
//AF_INET代表IPV4协议格式,SOCK_DGRAM代表以UDP数据报形式发送,0代表选择IPV4和UDP的默认协议
int fd=socket(AF_INET,SOCK_DGRAM,0);
if(fd<0){
cout<<"创建套接字失败"<<endl;
}
2)绑定端口号
在我的上一篇文章,我们以及明白绑定端口号加上IP才能确定互联网内的唯一一台主机,客户端可以不绑定端口号,这样子操作系统就会随机分配端口号,但是服务端不能这样,不然其他人无法主动连接服务端,因为其他人根本无法发现它,需要被别人第一次主动发现需要绑定端口号。现在我们来学习绑定端口号的接口。
scokfd就是我们前面使用socket接口创建的文件描述符。我们重点介绍接下来第二个参数,第三个参数是第二个参数的长度。
addr是结构体强转后得到的,它可以由IPV4结构体格式强转得到,也可以由IPV6格式强转得到,socketaddr_in是IPV4协议,socketaddr_un是IPV6协议。可以看下图理解
struct socketaddr里面的内容
struct socketaddr_in里面的内容
上图struct in_addr里面的内容
具体初始化和使用例子:
//IPV4结构体
struct sockaddr_in _addr;
//设置为IPV4协议
_addr.sin_family=AF_INET;
//端口号网络字节序
_addr.sin_port=htons(PORT);
//IP地址网络字节序,inet_addr函数将C风格字符串的IP地址形式转化成uint32_t的网络字节序类型
_addr.sin_addr.s_addr=inet_addr(IP);
//成功返回0,失败返回-1设置错误码,设置成功只能接收来自IP主机发送给PORT的信息
int result=bind(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
if(result!=0){
cout<<"绑定端口号失败"<<endl;
}
3)发送与接收数据
发送数据,UDP协议使用的是sendto接口
socketfd就是套接字文件描述符,buf是发送的数据地址,len是发送数据的长度,flag是位图,使用|可以实现对发送的方法控制
- 发送标志,可以是一个或多个标志的组合,用于修改
sendto
的行为。常见的标志包括:MSG_CONFIRM
:请求确认消息已被接收(某些实现可能不支持)。MSG_DONTROUTE
:避免路由,直接发送到本地接口。MSG_DONTWAIT
:非阻塞发送,如果操作会阻塞,则立即返回错误。MSG_EOR
:表示记录结束(对某些协议有意义)。MSG_MORE
:指示发送的数据是更大消息的一部分。
后面两个参数就不必多少,目标地址的信息和长度强转得来。最后成功返回发送数据的长度,失败返回-1,并设置错误码。
接收数据,UDP协议用的是recvfrom函数,
socketfd就是套接字文件描述符,buf是接收数据存放的地址,len是接收数据的最大长度,flag是位图,使用|可以实现对接收数据的方法控制
- 接收标志,可以是一个或多个标志的组合,用于修改
recvfrom
的行为。常见的标志包括:MSG_PEEK
:查看数据而不从队列中删除它。MSG_WAITALL
:请求接收完整的消息(对于某些协议可能不适用)。MSG_DONTWAIT
:非阻塞接收,如果操作会阻塞,则立即返回错误。MSG_TRUNC
:即使数据被截断也继续接收(通常与MSG_PEEK
一起使用)。MSG_CTRUNC
:如果控制消息被截断,则设置msg_flags
的MSG_CTRUNC
标志。
src_addr会返回发送数据的信息,如端口号,IP地址,addrlen是src_addr的长度。成功返回收到数据的长度,失败返回-1。
4)UDP简单的发送数据和接收数据服务器
中间可能有一个地方没讲清楚,bind函数不论接收,数据还是发送数据都最好设置,设置成功能接收你设置的主机发过来的特点端口号消息,sendto函数里面设置的是要发送给的人的IP和端口号。recvfrom函数里面的struct sockeaddr是接收消息的发送主机信息,方便你回信息和处理。
发送端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include<unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<iostream>
using namespace std;
#define PORT 8081
//本地环回通信测试
#define IP "127.0.0.1"
int main(){
//AF_INET代表IPV4协议格式,SOCK_DGRAM代表以UDP数据报形式发送,0代表选择IPV4和UDP的默认协议
int fd=socket(AF_INET,SOCK_DGRAM,0);
if(fd<0){
cout<<"创建套接字失败"<<endl;
return -1;
}
//不绑定端口号,操作系统随机分配
char msg[13]="hello world!";
struct sockaddr_in _send;
_send.sin_family=AF_INET;
_send.sin_port=htons(8080);
_send.sin_addr.s_addr=inet_addr(IP);
//给地址为IP主机8080端口号发送消息
int result=sendto(fd,(void*)msg,13,0,(struct sockaddr*)&_send,(socklen_t)sizeof(_send));
if(result<0){
cout<<"发送数据失败"<<endl;
return -1;
}
close(fd);
return 0;
}
接收端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include<unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<iostream>
using namespace std;
#define PORT 8080
int main(){
//AF_INET代表IPV4协议格式,SOCK_DGRAM代表以UDP数据报形式发送,0代表选择IPV4和UDP的默认协议
int fd=socket(AF_INET,SOCK_DGRAM,0);
if(fd<0){
cout<<"创建套接字失败"<<endl;
return -1;
}
//IPV4结构体
struct sockaddr_in _addr;
//设置为IPV4协议
_addr.sin_family=AF_INET;
//端口号网络字节序
_addr.sin_port=htons(PORT);
//接收所有主机的信息
_addr.sin_addr.s_addr=INADDR_ANY;
//成功返回0,失败返回-1设置错误码
int result=bind(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
if(result!=0){
cout<<"绑定端口号失败"<<endl;
return -1;
}
char msg[20];
struct sockaddr_in recv;
//必须写,不能为空。
socklen_t len=sizeof(recv);
result=recvfrom(fd,(void*)msg,20,0,(struct sockaddr*)&recv,&len);
if(result<0){
cout<<"接收数据数据失败"<<endl;
return -1;
}
for(int i=0;i<result;i++){
cout<<msg[i];
}
close(fd);
return 0;
}
二,TCP编程
1)创建套接字
创建套接字,与UDP创建套接字相似,只要把SOCK_DGRAM改为SOCK_STREAM
//SOCK_STREAM代表字节流,适用于TCP
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd<0){
cout<<"创建套接字失败"<<endl;
return -1;
}
2)绑定端口号
绑定端口号与UDP没有差别,就是接收来自指定的主机的连接请求,UDP是没有连接,需要发送消息时指定目的地址的。暂时简单理解就行。
struct sockaddr_in _addr;
//设置为IPV4协议
_addr.sin_family=AF_INET;
//端口号网络字节序
_addr.sin_port=htons(PORT);
//IP地址网络字节序,inet_addr函数将C风格字符串的IP地址形式转化成uint32_t的网络字节序类型
_addr.sin_addr.s_addr=inet_addr(IP);
//成功返回0,失败返回-1设置错误码
int result=bind(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
if(result!=0){
cout<<"绑定端口号失败"<<endl;
return -1;
}
3)使套接字进入监听状态
在TCP编程里面,创建套接字后并不能直接使用,TCP套接字只用来接收来自其他主机的连接请求,UDP发送完数据就不管了,是无连接的,TCP是面向连接的,双方会建立一个连接,也就是会为两台主机间创建单独的文件描述符,并且进行管理,这个文件描述符只能用来双方通信,而UDP可以实现一个文件描述符也就是socket就向所有主机发送消息。只有将套接字变成监听状态才会接收来自其他主机的连接。
第一个参数无需多言,就是我们使用socket函数创建的套接字,backlog是允许同时与多少台主机建立连接,也就是同时创建多少个通信的文件描述符,成功返回0,失败返回-1,并设置错误码。
//允许同时最大与三个主机建立连接
result=listen(fd,3);
if(result!=0){
cout<<"套接字启动监听失败"<<endl;
return -1;
}
4)获取成功建立连接的的文件描述符和主机信息
套接字进入监听状态后,我们需要获得建立连接的文件描述符,这样基于文件描述符才能和建立连接的主机通信,我们使用accept函数获取建立连接的消息,一般使用一个while循环来获取得到的多个连接信息。
第一个参数是套接字,第二个参数是连接主机的信息,第三个是第二个参数的长度,方便区分类型。成功返回建立连接的文件描述符,失败返回-1,并设置错误码。
while(1){
//这里不对对方主机信息进行处理,设置为空
int fd_net=accept(fd,NULL,NULL);
if(fd_net==-1){
cout<<"TCP连接失败"<<endl;
return -1;
}
//进行处理,发送或者接收数据
}
5)发送与接收数据
TCP可以使用UDP的sendto和recvfrom函数发送与接收数据,但一般不这么做,因为TCP以及建立连接了,每个连接文件描述符都只和一台主机通信,被唯一的四元组来标识的,这个四元组包括源IP地址、源端口号、目的IP地址和目的端口号。没必要使用这两个函数,这两个函数里面还需要包括目的主机地。一般使用send和write,read与recv。
flag常用标志
- MSG_DONTWAIT(或MSG_NONBLOCK)
- 作用:允许非阻塞操作。如果套接字被设置为非阻塞模式,并且发送缓冲区已满,则
send
函数会立即返回,而不是阻塞等待缓冲区空间可用。 - 返回值:在非阻塞模式下,如果发送缓冲区已满,
send
函数可能返回-1
,并设置errno
为EAGAIN
或EWOULDBLOCK
,表示资源暂时不可用。
- 作用:允许非阻塞操作。如果套接字被设置为非阻塞模式,并且发送缓冲区已满,则
- MSG_OOB(Out-of-Band Data)
- 作用:发送带外数据。带外数据通常用于发送紧急数据,这些数据会被接收方优先处理。然而,并非所有协议都支持带外数据,且其使用方式可能因协议而异。
- 限制:
MSG_OOB
标志通常仅适用于流式套接字(如SOCK_STREAM),而不适用于数据报套接字(如SOCK_DGRAM)。
- MSG_DONTROUTE
- 作用:勿将数据路由出本地网络。这个标志告诉系统不要通过网关或路由器发送数据,而只在本地网络上发送。然而,并非所有系统都支持这个标志,且其效果可能因系统而异。
成功返回发送数据大小,失败返回-1,设置错误码。
fd是文件描述符,也就是accept函数的返回值,buf被发送的数据,count是发送的大小。
flag常用标志
- MSG_PEEK
- 作用:查看接收队列中的数据,但不从队列中移除它们。这允许调用者在不实际消耗数据的情况下检查是否有数据可读。
- 使用场景:在需要多次读取同一份数据或检查数据是否到达时非常有用。
- MSG_WAITALL
- 作用:阻塞调用,直到接收到指定长度的数据或连接关闭。然而,需要注意的是,并非所有系统都支持这个标志,且其行为可能因系统而异。
- 使用场景:在需要确保接收到完整消息时非常有用,但应谨慎使用,因为它可能导致程序在数据不足时长时间阻塞。
- MSG_DONTWAIT(或MSG_NONBLOCK)
- 作用:在非阻塞模式下接收数据。如果当前没有数据可读,则立即返回,而不是阻塞等待。
- 使用场景:在需要避免阻塞等待数据到达时非常有用,例如在非阻塞I/O或事件驱动的编程模型中。
- MSG_OOB
- 作用:接收带外数据(Out-of-Band Data)。带外数据通常用于发送紧急数据,这些数据会被接收方优先处理。然而,并非所有协议都支持带外数据。
- 使用场景:在需要处理紧急数据或优先级较高的消息时非常有用,但应确保所使用的协议支持带外数据。
- MSG_TRUNC
- 作用:如果接收到的数据长度超过了缓冲区长度,则只返回缓冲区长度的数据,并截断多余的数据。然而,需要注意的是,并非所有系统都支持这个标志。
- 使用场景:在需要限制接收数据的大小或处理不完整数据时可能有用。
- MSG_CTRUNC
- 作用:类似于
MSG_TRUNC
,但用于控制信息的截断。如果接收到的控制信息长度超过了缓冲区长度,则只返回缓冲区长度的控制信息。 - 使用场景:在处理带有控制信息的套接字时可能有用。
- 作用:类似于
- MSG_ERRQUEUE
- 作用:接收错误信息。如果接收到的数据包出现错误,则会将错误信息放入错误队列中,可以通过此标志来接收这些错误信息。
- 使用场景:在需要处理套接字错误或诊断网络问题时非常有用。
fd是文件描述符,也就是accept函数的返回值,buf存放数据,count是接收数据的最大大小,防止越界。
6)连接其他主机
上面我们只说了如何被动连接其他主机,但我们该如何主动连接其他主机呢?使用connect函数我们主动连接其他主机,是需要设置协议和IP,端口号信息的。注意connect连接成功之后这个scokfd就被占用了,用来后续的通信,需要继续使用socket函数创建与多台主机建立连接。这是与accept函数不同的地方,accept函数是创建了新的文件描述符,sockfd还可以继续监听。
成功返回0,失败返回-1,其他这些前面都讲过,老生常谈了,无需多言。
7)TCP简单的发送数据和接收数据服务器
服务端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include<unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<iostream>
using namespace std;
#define PORT 8080
#define IP "127.0.0.1"
int main(){
//SOCK_STREAM代表字节流,适用于TCP
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd<0){
cout<<"创建套接字失败"<<endl;
return -1;
}
//IPV4结构体
struct sockaddr_in _addr;
//设置为IPV4协议
_addr.sin_family=AF_INET;
//端口号网络字节序
_addr.sin_port=htons(PORT);
//IP地址网络字节序,inet_addr函数将C风格字符串的IP地址形式转化成uint32_t的网络字节序类型
_addr.sin_addr.s_addr=inet_addr(IP);
//成功返回0,失败返回-1设置错误码
int result=bind(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
if(result!=0){
cout<<"绑定端口号失败"<<endl;
return -1;
}
//允许同时最大与三个主机建立连接
result=listen(fd,3);
if(result!=0){
cout<<"套接字启动监听失败"<<endl;
return -1;
}
while(1){
//这里不对对方主机信息进行处理,设置为空
int fd_net=accept(fd,NULL,NULL);
if(fd_net==-1){
cout<<"TCP连接失败"<<endl;
return -1;
}
char msg[13]="hello world!";
//进行处理,发送或者接收数据
result=send(fd_net,(void*)msg,13,0);
close(fd);
}
return 0;
}
客户端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include<unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<iostream>
using namespace std;
#define PORT 8080
#define IP "127.0.0.1"
int main(){
//SOCK_STREAM代表字节流,适用于TCP
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd<0){
cout<<"创建套接字失败"<<endl;
return -1;
}
//IPV4结构体
struct sockaddr_in _addr;
//设置为IPV4协议
_addr.sin_family=AF_INET;
//端口号网络字节序
_addr.sin_port=htons(PORT);
_addr.sin_addr.s_addr=inet_addr(IP);
int result=connect(fd,(struct sockaddr*)&_addr,(socklen_t)sizeof(_addr));
if(result==-1){
cout<<"连接主机失败"<<endl;
return -1;
}
char msg[20];
result=recv(fd,msg,20,0);
for(int i=0;i<result;i++){
cout<<msg[i];
}
close(fd);
return 0;
}
创造不易,我为人人,人人为我,如果大家有所收获的话可以点赞加关注,下一篇文章将会着重讲TCP与UDP的特性。