linux-网络编程套接字
网络通信是两端通信:
其中一端叫客户端;另-端叫服务端
通信双方中主动发起请求的这一方是客户端-必须知道服务端在哪里
通信双方中被动接收请求的这一方是服务端-- 必须告诉客户端自己在哪里(通常都是一个固定地址)
传输层有两个协议: TCP/UDP 需要用户告诉操作系统,到底传输层应该用哪个
- UDP (用户数据报协议) :
无连接,不可靠,面向数据报
传输性能高,但是不保证可靠传输
udp适用场景:实时性要求极高,但是安全性要求不是很高的场景—视频传输- TCP (传输控制协议):
面向连接, 可靠传输,面向字节流
传输性能较低,但是保证可靠传输
tcp适用场景:安全性要求极高—文件传输
socket 常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
//接收数据(TCP/UDP, 客户端 + 服务器)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
//发送数据(TCP/UDP, 客户端 + 服务器)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
基于UDP协议的socket客户端与服务端的通信编程
udp通信流程:
- 客户端: 1. 创建套接字–> 2. 为套接字绑定地址(客户端不推荐主动绑定) --> 3. 发送数据–> 4.接收数据–> 5.关闭套接字
- 服务端: 1. 创建套接字–>2.为套接字绑定地址–>3. 接收数据–>4.发送数据–>5.关闭套接字
使用C++封装一个udpsocket类,来实现socket的简单操作:
三个文件:
- udpsocket.hpp 中封装类
- udp_cli.cpp 实现udp客户端通信
- udp_srv.cpp 实现udp服务端通信
udpsocket.hpp
/*===============================================================
* 描 述:封装UdpSocket类,实例化对象,向外提供简单的socket接口
* 1. 创建套接字
* 2. 为套接字绑定地址信息
* 3. 发送数据
* 4. 接收数据
* 5. 关闭套接字
================================================================*/
#include <iostream>
#include <string>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define CHECK_RET(q) if((q)==false){return -1;}
class UdpSocket{
private:
int _sockfd;
public:
bool Socket() {
//int socket(int domain, int type, int protocol);
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (_sockfd < 0) {
std::cerr << "socket error\n";
return false;
}
return true;
}
bool Bind(const std::string &ip, const uint16_t port) {
//bind(int sockfd, struct sockaddr *addr,socklen_t addrlen)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
//uint16_t htons(uint16_t hostshort);
//将主机字节序的16位数据,转换位网络字节序数据返回
addr.sin_port = htons(9000);
//192.168.122.132 -> 0xc0a87a84
//in_addr_t inet_addr(const char *cp);
//将点分十进制字符串IP地址转换为网络字节序IP地址
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret;//返回值,仅仅用来判断bind是否成功
socklen_t len = sizeof(struct sockaddr_in);
ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
std::cerr << "bind error\n";
return false;
}
return true;
}
bool Send(const std::string &data, const std::string &peer_ip, const uint16_t peer_port)
//给对端peer_ip的peer_port端口发送data数据
{
//ssize_t sendto(int sockfd, const void *buf, size_t len,
//int flags,struct sockaddr *dest_addr, socklen_t addrlen);
struct sockaddr_in addr;//定义一个结构体struct sockaddr_in addr,
//然后用要发送到的对端ip地址和对端ip地址中的port端口号
//分别对addr.sin_family,addr.sin_port,addr.sin_addr.s_addr进行赋值
addr.sin_family = AF_INET;
addr.sin_port = htons(peer_port);
addr.sin_addr.s_addr = inet_addr(peer_ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_sockfd, &data[0], data.size(), 0, (struct sockaddr*) &addr, len);
//通过_sockfd对应的socket结构体中的源ip的源端口
//将data中data.size()长度的数据发送到对端地址(struct sockaddr*) &addr
//对端是以(struct sockaddr*) &addr为地址的主机
//通过最后一个参数len和地址类型struct sockaddr_in addr
//可以判断出目的ip地址和目的port端口
//就完成了数据从源ip地址的port端口到目的端ip地址的port端口的发送
if (ret < 0) {
std::cerr << "sendto error\n";
return false;
}
return true;
}
bool Recv(std::string &buf, std::string &peer_ip, uint16_t &peer_port)
//Recv中参数均为输出参数
//定义一个结构体struct sockaddr_in peer_addr来接收该数据是从哪个ip地址的哪个port端口发送过来的
{
//ssize_t recvfrom(int sockfd, void *buf, size_t len,
//int flags,struct sockaddr *src_addr, socklen_t *addrlen)
//成功:返回实际接收的数据长度 , 失败:-1
struct sockaddr_in peer_addr;//定义一个结构体struct sockaddr_in peer_addr来接收该数据是从哪个ip地址的哪个port端口发送过来的
socklen_t len = sizeof(struct sockaddr_in);
char tmp[4096] = {0};
int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len);
//从_sockfd中对应的socket结构体的接收队列中,取出一条数据放到tmp中,
//然后保存发送端的地址到参数[结构体(struct sockaddr*)&peer_addr]中
//通过对(struct sockaddr*)&peer_addr对端(发送端)地址的解析
//对三个参数进行输出
//将对端地址peer_ip和对端端口号peer_port赋值
//将tmp中的数据放入到buf中
if (ret < 0) {
std::cerr << "recvfrom error\n";
return false;
}
//char *inet_ntoa(struct in_addr in);
//将网络字节序IP地址转换为点分十进制字符串IP地址
//uint16_t ntohs(uint16_t netshort);
//将网络字节序的16位数据转换为主机字节序数据
peer_ip = inet_ntoa(peer_addr.sin_addr);
peer_port = ntohs(peer_addr.sin_port);
buf.assign(tmp, ret);
return true;
}
void Close() {
close(_sockfd);
}
};
udp_cli.cpp
#include "udpsocket.hpp"
#include <sstream>
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./udp_cli ip port\n";
return -1;
}
uint16_t port;
std::string ip = argv[1];
std::stringstream tmp;
tmp << argv[2];
tmp >> port;
UdpSocket sock;
CHECK_RET(sock.Socket());
//客户端不推荐用户主动绑定固定地址,因为一个端口只能被一个进程占用
//因此一旦端口固定,这个客户端程序就只能启动一个
while(1) {
std::string buf;
std::cin >> buf;
//当socket还没有绑定地址,这时候操作系统在发送之前可以检测到
//这时候操作系统会为socket选择一个合适的地址和端口进行绑定
sock.Send(buf, ip, port);//将buf中的数据通过socket对应的结构体发送到ip主机的port端口
buf.clear();
sock.Recv(buf, ip, port);//Recv中的buf,ip和port都是输出参数
//表示从哪个ip的哪个port端口发送到的数据,存到buf中
std::cout << "server say:" << buf << std::endl;
}
sock.Close();
return 0;
}
udp_srv.cpp
#include "udpsocket.hpp"
#include <sstream>
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./udp_srv 192.168.122.132 9000\n";
return -1;
}
//用ip和port来接收cin输入的ip和port
uint16_t port;
std::string ip = argv[1];
std::stringstream tmp;
tmp << argv[2];
tmp >> port;
UdpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Bind(ip, port));
while(1) {
std::string buf;
std::string peer_ip;
uint16_t peer_port;
sock.Recv(buf, peer_ip, peer_port);//Recv中的buf,ip和port都是输出参数,故定义三个变量来接受
//表示从哪个ip的哪个端口发送到的数据,存到buf中
std::cout << "client-["<<peer_ip<<":"<<peer_port<<"] say:"
<<buf<<std::endl;
buf.clear();
std::cin >> buf;
sock.Send(buf, peer_ip, peer_port);//将buf中的数据通过socket对应的结构体发送到peer_ip主机的peer_port端口
}
sock.Close();
}
代码运行结果:【注意先启动服务端】
基于TCP协议的socket客户端与服务端的通信编程
- 客户端: 1. 创建套接字–> 2.绑定地址(不推荐主动绑定)–> 3.向服务端发起连接–> 4.发送数据–> 5. 接收数据–> 6.关闭套接字
- 服务端: 1. 创建套接字–> 2.绑定地址–> 3.开始监听–> 4.获取已完成连接–> 5.通过获取的已完成连接socket接收数据–> 6.通过获取的已完成连接socket发送数据–> 7.关闭套接字
同样使用C++封装tcpsocket类来实现socket简单操作:
- tcpsocket.hpp : 封装tcpsocket类
- tcp_cli.cpp : 实现tcp客户端通信
- tcp_srv.cpp : 实现tcp服务端通信
tcpsocket.hpp
/*===============================================================
* 描 述:封装一个tcpsocket类,向外提供简单的套接字接口
* 1. 创建套接字
* 2. 为套接字绑定地址信息
* 3. 开始监听
* 4. 向服务端发起连接请求
* 5. 服务端获取新建连接
* 6. 发送数据
* 7. 接收数据
* 8. 关闭套接字
================================================================*/
#include <iostream>
#include <sstream>
#include <string>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define CHECK_RET(q) if((q)==false){return -1;}
class TcpSocket
{
private:
int _sockfd;
public:
void SetFd(int fd) {
_sockfd = fd;
}
int GetFd() {
return _sockfd;
}
bool Socket() {
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//在内核中创建套接字,就是一个struct socket结构体
//建立进程与网卡之间的联系
//但是返回值是int,返回的是文件描述符,只是一个套接字操作句柄
if (_sockfd < 0) {
std::cerr << "socket error\n";
return false;
}
return true;
}
int str2int(const std::string &str){
int num;
std::stringstream tmp;
tmp << str;
tmp >> num;
return num;
}
bool Bind(const std::string &ip, const std::string &port) {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(str2int(port));
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);//将_sockfd对应的socket结构体与addr地址绑定,
//表示该套接字的socket结构体应该处理特定ip地址中的特定port端口的数据
if (ret < 0) {
std::cerr << "bind error\n";
return false;
}
return true;
}
bool Listen(const int backlog = 5) {
//int listen(int sockfd, int backlog);
//开始监听:通知操作系统,可以开始接收客户端的连接请求了,
//并且完成三次握手建立连接过程
//tcp的面向连接,有一个三次握手建立连接过程
//backlog:客户端最大并发连接数(同一时间最多接收多少个客户端
//新连接请求)
int ret = listen(_sockfd, backlog);
if (ret < 0) {
std::cerr << "listen error\n";
return false;
}
return true;
}
bool Connect(const std::string &srv_ip, const std::string &srv_port)//和bind参数相同,不过bind是绑定(自己)地址,connect是对端地址
{
//int connect(int sockfd, sockaddr *addr,socklen_t addrlen)
//addr: 服务端地址信息
//addrlen: 地址信息长度
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(str2int(srv_port));
addr.sin_addr.s_addr = inet_addr(srv_ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr*)&addr, len);//想要建立与对端地址为addr的ip+端口的新连接
//_sockfd是创建套接字的返回句柄(int型)
//_sockfd这个句柄中包含了(struct sockaddr*)&addr结构体
//此地址是自己已经bind绑定的地址
//或者是由内核随机分配的地址
//而这里参数中的(struct sockaddr*)&addr表示的是想要建立连接的对端地址信息
if (ret < 0) {
std::cerr << "connect error\n";
return false;
}
return true;
}
bool Accept(TcpSocket &clisock, std::string *ip = NULL, uint16_t *port = NULL) //注意参数
{
//int accept(int sockfd, sockaddr *addr, socklen_t *addrlen)
//sockfd: 监听套接字描述符
//addr: 客户端地址信息
//addrlen: 地址信息长度
//返回值:返回新建连接的socket描述符-与客户端进行数据通信
struct sockaddr_in cliaddr;
socklen_t len = sizeof(struct sockaddr_in);
int newfd = accept(_sockfd, (sockaddr*)&cliaddr, &len);//新创建一个套接字来专门和这个cliaddr地址的客户端进行收发数据处理
//参数(sockaddr*)&cliaddr是输出参数,
//表示接下来让该_sockfd套接字与该cliaddr地址进行一对一数据收发
if (newfd < 0) {
std::cerr << "accept error\n";
return false;
}
clisock.SetFd(newfd);//参数clisock也是一个输出参数,接收新的套接字描述符newfd
//保存到新类clisock中,后续操作针对clisock类就相当于操作新的套接字描述符newfd
//当ip,port两个参数传空时,用accept函数中的输出参数里面的地址的ip+port进行赋值即可
if (ip != NULL) {
*ip = inet_ntoa(cliaddr.sin_addr);
}
if (port != NULL) {
*port = ntohs(cliaddr.sin_port);
}
return true;
}
bool Send(std::string &data) {
//ssize_t send(int sockfd, void *buf, size_t len, int flags)
//sockfd: 套接字描述符(服务端是新建连接的socket描述符)
//buf: 要发送的数据
//len: 要发送的数据长度
//flags: 0-默认阻塞发送
//返回值: 成功-返回实际发送的数据长度;失败-返回-1
int ret = send(_sockfd, &data[0], data.size(), 0);
if (ret < 0) {
std::cerr << "send error\n";
return false;
}
return true;
}
bool Recv(std::string &buf) {
//ssize_t recv(int sockfd, void *buf, size_t len, int flags)
//flags:
// 0-默认阻塞接收
// MSG_PEEK:从缓冲区取数据,但是数据并不从缓冲区移除
//返回值:>0:实际接收的数据长度 ==0:连接断开 <0:错误
char tmp[4096];
int ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0) {
std::cerr << "recv error\n";
return false;
}else if (ret == 0) {
std::cerr << "connect shutdown\n";
return false;
}
buf.assign(tmp, ret);
return true;
}
bool Close() {
close(_sockfd);
}
};
tcp_cli.cpp
/*===============================================================
* 描 述:tcp客户端通信流程
* 1. 创建套接字
* 2. 为套接字绑定地址信息(不推荐用户主动绑定)
* 3. 向服务端发起连接请求
* 4. 发送数据
* 5. 接收数据
* 6. 关闭套接字
================================================================*/
#include <stdio.h>
#include <signal.h>
#include "tcpsocket.hpp"
void sigcb(int signo)
{
printf("recv a signo SIGPIPE --- conect shutdown\n");
}
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./tcp_cli ip port\n";
return -1;
}
signal(SIGPIPE, sigcb);
TcpSocket sock;
/*1. 创建套接字*/
CHECK_RET(sock.Socket());
/*2. 为套接字绑定地址信息(不推荐用户主动绑定)*/
/*3. 向服务端发起连接请求*/
CHECK_RET(sock.Connect(argv[1], argv[2]));
while(1) {
/*4. 发送数据*/
std::string buf;
std::cout << "client say: ";
fflush(stdout);
std::cin >> buf;
sock.Send(buf);
/*5. 接收数据*/
buf.clear();
sock.Recv(buf);
std::cout << "server say: " << buf << std::endl;
}
/*6. 关闭套接字 */
sock.Close();
return 0;
}
tcp_srv.cpp
/*===============================================================
* 描 述:tcp服务端通信流程
* 1. 创建套接字
* 2. 为套接字绑定地址信息
* 3. 开始监听
* 4. 获取已完成连接socket
* 5. 通过获取的新建socket与客户端进行通信-接收数据
* 6. 发送数据
* 7. 关闭套接字
================================================================*/
#include <stdio.h>
#include "tcpsocket.hpp"
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./tcp_srv ip port\n";
return -1;
}
TcpSocket lst_sock;
/*1. 创建套接字*/
CHECK_RET(lst_sock.Socket());
/*2. 为套接字绑定地址信息*/
CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
/*3. 开始监听*/
CHECK_RET(lst_sock.Listen());
while(1){
/*4. 获取已完成连接socket*/
TcpSocket clisock;//创建一个新类来接受accept的输出参数,新的套接字描述符对应的类
bool ret = lst_sock.Accept(clisock);
if (ret == false) {
continue;
}
/*5. 通过获取的新建socket与客户端进行通信-接收数据*/
std::string buf;
ret = clisock.Recv(buf);//后续收发数据的操作都是在类clisock上进行操作
if (ret == false) {
clisock.Close();//1.关闭Accept参数中的新类clisock中的套接字
continue;
}
std::cout << "client say: " << buf << std::endl;
/*6. 发送数据*/
buf.clear();
std::cout << "server say: ";
fflush(stdout);
std::cin >> buf;
clisock.Send(buf);
}
/*7. 关闭套接字 */
lst_sock.Close();//2.关闭用来监听连接的套接字
return 0;
}
运行后发现当前服务端程序只能与一个客户端通信一次
【注意先启动服务端】
可以看到服务端阻塞:
原因:因为服务端不知道客户端的新连接请求/数据什么时候到来,因此在程序写死的情况下,就会阻塞在recv或者accept两个接口处,导致流程无法继续【代码理解就是封装类中accept以后才收发一次数据,也就是说每次新连接来了之后才能进行一次收发数据,所以这样写,代码有缺陷】
解决方案:服务端为每一个新的客户端都创建一个新的进程/线程来专门与该客户端进行通信。【就是用一个线程/进程负责客户端已完成连接获取功能,然后为每个客户端新建一个线程/进程处理独立通信】
将tcp服务端的通信改成多进程版本:
tcp_progress.cpp
/*===============================================================
* 描 述:tcp服务端通信流程
* 1. 创建套接字
* 2. 为套接字绑定地址信息
* 3. 开始监听
* 4. 获取已完成连接socket
* 5. 通过获取的新建socket与客户端进行通信-接收数据
* 6. 发送数据
* 7. 关闭套接字
================================================================*/
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include "tcpsocket.hpp"
void sigcb(int no) {
//如果有僵尸进程可以处理,就一直处理
//如果没有子进程退出了则waitpid返回0,退出循环
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./tcp_srv ip port\n";
return -1;
}
signal(SIGCHLD, sigcb);
TcpSocket lst_sock;
/*1. 创建套接字*/
CHECK_RET(lst_sock.Socket());
/*2. 为套接字绑定地址信息*/
CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
/*3. 开始监听*/
CHECK_RET(lst_sock.Listen());
while(1){
/*4. 获取已完成连接socket*/
TcpSocket clisock;
bool ret = lst_sock.Accept(clisock);//表示接收到一个新连接
if (ret == false) {
continue;
}
if (fork() == 0)//接收到一个新连接就创建一个进程来和其收发数据
{
while(1) {
/*5. 通过获取的新建socket与客户端进行通信-接收数据*/
std::string buf;
clisock.Recv(buf);
std::cout << "client say: " << buf << std::endl;
/*6. 发送数据*/
buf.clear();
std::cout << "server say: ";
fflush(stdout);
std::cin >> buf;
clisock.Send(buf);
}
clisock.Close();
}
clisock.Close();
}
/*7. 关闭套接字 */
lst_sock.Close();
return 0;
}
或者
将tcp服务端的通信改成多线程版本:
tcp_thread.cpp
/*===============================================================
* 描 述:tcp服务端通信流程
* 1. 创建套接字
* 2. 为套接字绑定地址信息
* 3. 开始监听
* 4. 获取已完成连接socket
* 5. 通过获取的新建socket与客户端进行通信-接收数据
* 6. 发送数据
* 7. 关闭套接字
================================================================*/
#include <stdio.h>
#include <pthread.h>
#include "tcpsocket.hpp"
void *thr_start(void *arg)
{
TcpSocket *clisock = (TcpSocket*)arg;
while(1) {
/*5. 通过获取的新建socket与客户端进行通信-接收数据*/
std::string buf;
clisock->Recv(buf);
std::cout << "client say: " << buf << std::endl;
/*6. 发送数据*/
buf.clear();
std::cout << "server say: ";
fflush(stdout);
std::cin >> buf;
clisock->Send(buf);
}
clisock->Close();
delete clisock;
return NULL;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./tcp_srv ip port\n";
return -1;
}
TcpSocket lst_sock;
/*1. 创建套接字*/
CHECK_RET(lst_sock.Socket());
/*2. 为套接字绑定地址信息*/
CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
/*3. 开始监听*/
CHECK_RET(lst_sock.Listen());
while(1){
/*4. 获取已完成连接socket*/
TcpSocket *clisock = new TcpSocket();
bool ret = lst_sock.Accept(*clisock);//表示接收到一个新连接
if (ret == false) {
continue;
}
pthread_t tid;//接收到一个新连接就创建一个线程来和其收发数据
pthread_create(&tid, NULL, thr_start, (void*)clisock);
pthread_detach(tid);
}
/*7. 关闭套接字 */
lst_sock.Close();
return 0;
}
这样客户端和服务端就可以正常通信了:【注意先启动服务端】