网络套接字编程,主要是针对于传输层,因为传输层有两个协议tcp/udp,因此我们必须选择其一进行数据传输,选哪个那,这种时候我们必须就要明了两个协议的优缺点,视使用场景而定。
TCP协议
优点: 可靠传输,并且传输灵活
缺点: 传输速度低,数据粘包
UDP协议
优点: 传输速度快、无粘包
缺点: 不可靠
针对数据安全要求高的场景(文件传输)使用TCP保证数据的可靠。
针对数据安全性要求不是很高,但是实时性要求高的场景(视频传输),使用UDP保证传输的速度。
网络编程涉及到对网卡的操作,因此操作系统就提供了一套接口来供我们操作——socker接口,网络编程中分了两个端:客户程序端、服务端程序。
网络编程中,客户端是主动的一方(永远是客户端首先向服务端发起请求),并且客户端必须知道服务端的地址信息(ip+port),并且服务端必须得在这个指定的地址上等着别人。
socket是一套接口,用于网络编程的接口,同时socket也是一个数据结构。想要开始网络编程,就需要先创建一个套接字,也就是说对于我们网络编程来说,第一步永远是创建套接字,套接字创建成功后,我们才可以通过对套接字的操作,来完成网络上数据的传输。
int socket(int domain, int type, int protocol); //创建套接字
domain //地址域
AF_INET //IPE4
type //套接字类型
SOCK_STREAM //流式套接字 tcp
SOCK_DGRAM //数据报套接字 udp
protocol //协议类型
IPPROTO_TCP // tcp协议 6
IPPROTO_UDP //udp协议 17
返回值 //套接字描述符,非负整数
//失败返回 -1
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。
声明去网卡接受数据时接受的是哪一部分数据(因为网卡上可能会有很多的数据)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //为套接字绑定地址信息
sockfd //套接字描述符
addr //地址信息,要绑定的地址信息 构造出IP地址加端口号
addrlen //地址信息长度
返回值 //成功 0
//失败 -1
//编程中可能会使用的一些函数
unit16_t htons(unit16_t hostshort);
//将一个短整型(16位)数据从主机字节序转换为网络字节序
int addr_t inet_addr(const char *cp)
//将一个点分十进制的字符串ip地址转化为网络字节序
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
//接受
ssize_t recvform(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sockfd //套接字描述符,告诉操作系统接受那里的数据
buf //用于存储接受的数据
len //想要接受的数据长度
flags //发送标志 0 默认阻塞
MSG_PEEK //接收数据后数据并不会从缓冲区删除
//场景:探测性获取数据
src_addr //发送端的地址信息
addrle //地址信息长度/实际获取地址信息长度
返回值 //实际读取到的数据字节长度 失败 -1
//发送
ssize_t sendto(int sockfd, const void *buf, size_t len, int flag, struct sockaddr *dest_addr, socklen_t *addrlen);
sockfd //套接字描述符,发送数据的时候是通过这个socket所绑定的地址来发送
buf //要发送的数据
len //要发送的数据长度
flag //发送标志 0 默认阻塞
MSG_PEEK //接收数据后数据并不会从缓冲区删除
//场景:探测性获取数据
dest_addr //数据要发送到的对端地址信息
addrle //地址信息长度
返回值 //实际发送到的数据字节长度 失败 -1
close(int sockfd);
客户端与服务端在套接字编程中所使用的函数相同
//服务端
int main()
{
//创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0)
{
printf(error);
return - 1;
}
//为套接字绑定地址信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = 9000; //htons(9000)
addr.sin_addr.s_addr = inet_addr(""); //ip地址,字符串
//inet_pton(AF_INET, "", &addr.sin_addr.s_addr);
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(sockfd, (struct sockaddr*)&addr, len); //绑定不一定成功
if (ret < 0)
{
perror(); //自动恢复资源
close(sockfd);
return -1;
}
//接受数据
while (1)
{
char buff[1024] = { 0 };
struct sockaddr_in cli_addr;
len = sizeof(struct sockaddr_in);
ssize_t rlen = recvfrom(sockfd, buff, 1023, 0, (struct sockaddr*)&cli_addr, &len);
if (rlen < 0)
{
perror();
close(sockfd);
return -1;
}
printf("client[%s:%d] say:%s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buff); //打印一下客户端信息
//发送消息
memset(buff, 0x00, 1024);
scanf("%s", buff);
sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&cli_addr, len);
}
close(sockfd);
return 0;
}
//客户端
int main()
{
//1.
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0)
{
printf(error);
close(sockfd);
return -1;
}
//2.
//需要定义服务端的地址信息
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9000);
serv_addr.sin_addr.s_addr = inet_addr(""); //ip地址,字符串
//inet_pton(AF_INET, "", &addr.sin_addr.s_addr);
socklen_t len = sizeof(struct sockaddr_in);
//3.4.
while (1)
{
char buff[1024] = { 0 };
scanf("%s", buff);
sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&serv_addr, len); //客户端 与 服务端只有一个
memset(buff, 0x00, 1024);
ssize_t r_len = recvfrom(sockfd, buff, 1023, 0, (struct sockaddr*)&serv_addr, &len);
if (r_len < 0)
{
perror();
close(sockfd);
return -1;
}
printf(buff);
}
close(sockfd);
return 0;
}
服务端在创建套接字与为套接字绑定地址信息、关闭套接字时所使用的函数与UDP编程相同
int listen(int sockefd, int backlog);
sockefd //套接字描述符
backlog //一个整形数字 用以定义一个挂起的连接队列最大结点数,表示同一时间的一个并发连接数(同一时间能够接受多少个新客户端连接)
//定义已完成连接队列的最大结点数
//每一个客户端都会创建新的socket,一个新连接建立连接有一个过程,如果这个新的连接已经完成三次握手过程,就将这个新的socket放到这个队列中
//这个backlog决定了同一时间的最大并发连接数
返回值 //失败:返回-1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd //套接字描述符
addr //进连接的客户端地址信息
addrlen //用于确定要获取地址信息的长度,接受实际长度
//输入输出复合型参数,
//传出参数,返回链接客户端地址信息,含IP地址和端口号
返回值 //新建的socket连接的套接字描述符,失败:-1
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
accept的理解
//发送数据
ssizet_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd //套接字描述符
buf //发送的数据
len //发送数据的长度
flags //0 默认阻塞
返回值 //实际发送的数据长度, 失败 -1
//接受数据
ssizet_t recv(int sockfd, const void *buf, size_t len, int flags);
sockfd //套接字描述符
buf //接受的数据
len //接受数据的长度
flags //0 默认阻塞
返回值 //>0 实际接受的数据长度
//==0 链接断开
//-1 出错
客户端在TCP套接字编程中使用的函数多数与服务端相同
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd //套接字描述符
addr //服务端地址信息 传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen //地址信息长度
返回值 //失败返回-1,成功0
#define CHECK_RET(q) if((q) == false){return false;}
class TcpSocket
{
private:
int _sockfd;
//int port;
//std::string ip;
public:
//初始弧
//构造函数,初始化port与ip、socket(-1)
//析构函数
//1.创建套接字
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0)
{
perror();
//std::cerr<<""<_sockfd = newfd;
return true;
}
//5.发送/接受
bool Send(char *buf, size_t len)
{
int slen = 0;
while (slen < len)
{
int ret = send(_sockfd, buf + slen, len - slen, 0);//未发完继续发
if (ret < 0)
{
return false;
}
slen += len;
}
return true;
}
bool Recv(char* buf, size_t *len = NULL)
{
int ret rlen = 0;
if (len)
{
while (rlen < *len)
{
ret = recv(_sockfd, buf + rlen, *len - rlen, 0);
if (ret < 0)
{
if (errno == EAGAIN || errno == EINTR) //EAGAIN:缓冲区没有数据 EINTR:接受数据的过程被信号大端
{
continue;
}
return false;
}
else if (ret == 0)
{
printf();
return false;
}
rlen += ret;
}
}
else
{
ret = recv(_sockfd, buf, 1024, 0);
if (*len < 0)
{
return false;
}
if (len)
*len = ret;
}
return true;
}
//关闭套接字
bool Close()
{
close(_sockfd);
return true;
}
};
//服务端
//sock是专门用于获取客户端新连接的socket,称之为监听socket
//client是客户端新建的socket,专门用于跟客户端进行数据传输
int main()
{
if (argc != 3)
{
printf();
return -1;
}
str::string ip = argv[1];
unit16_t port = atoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.Socket()); //1
CHECK_RET(sock.Bind(ip, port)); //2
CHECK_RET(sock.Listen()); //3
while (1)
{
TcpSocket client;
std::string ip;
unint16_t port;
if (sock.Accept(&client, &ip, &port) == false)
{
continue;
}
client.Recv(buff);
printf();
memset(buff, 0x00, 1024);
fflush(stdout);
scanf();
client.Send(buff, strlen(buff));
}
sock.Close();
return 0;
}
//客户端
int main()
{
if (argc != 3)
{
printf();
return -1;
}
str::string ip = argv[1];
unit16_t port = atoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.Socket()); //1
CHECK_RET(sock.Connect(ip,port));//2
while (1)
{
char buff[1024] = { 0 };
fflush(stdout);
scanf();
sock.Send(buff, strlen(buff));
memset(buff, 0x00, 1024);
sock.Recv(buff);
printf();
}
sock.Close();
return 0;
}
对于接受端来说,recv返回值如果是0,就代表断开了。
对于发送端来说,send会触发Broken pipe异常,接受SIGPIPE信号,导致程序退出(对于大多是程序来说,连接断开了,不应该退出程序,而是重新连接,所以需要对SIGPIPE信号自定义处理方式)。