Linux网络——套接字编程

前面的概念学习完了,就应该来实践一下了,所以下面开始敲代码,我们来看一看网络编程。

目录

前情概要

套接字

网络字节序

struct sockaddr

客户端与服务端

UDP编程

流程

接收与发送

实战演练

TCP编程

流程

建立连接

接收与发送

实战演练

HTTP应用

HTTP服务器

结束


前情概要

在说编程时,我们先把准备工作做好,就像做饭,我们先把原料弄好,毕竟磨刀不误砍柴功。

  • 套接字

既然是套接字编程,我们先理解什么是套接字。

套接字是是一个文件描述符,描述为socket,而socket的中文意思又是插座。所以你可以把socket当作一个三孔插座。为什么是三孔,请看下面的一个调用接口。

#include //头文件

int socket(int domain, int type, int protocol);

//返回值:成功返回套接字,失败返回-1

这个是创建socket套接字的接口,其中有三个参数:

  1. domain:中文意思,领域,这个在这里叫地址域,它有多种取值,AF_INET:IPv4,AF_INET6:IPv6。我们用AF_INET就好。
  2. type:类型,常见套接字有流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)。流式套接字就是TCP用的,数据报套接字是UDP用的。
  3. protocol:协议,有常用的三种取值,数字0 使用默认协议,就是前面的类型是啥,后面的协议就是啥,所以一般就是0;IPPROTO_TCP 或者 数字6就是TCP协议;IPPROTO_UDP或者数字17就是UDP协议。

其实这个套接字就是一个文件的描述符,而文件里面写入了一个IP协议和一个传输协议。后续还会写上更多的东西,就类似我们的IO操作。

这是domian的取值

Linux网络——套接字编程_第1张图片

  • 网络字节序

在C语言阶段,我们学习了字节序。字节序就是数据的每个字节在我们内存上的存储顺序。记得C语言阶段我们这样说的,数据的高位存在低地址上,称为大端序,相反则是小端序。以前我们是通过一个4个字节int型的数据,用一个字节的char来取一个字节来验证机器是什么字节序。在复习一下,例如:

Linux网络——套接字编程_第2张图片

字节序取决于CPU架构,除了我们平时所见的英特尔,还有其他的牌子,如AMD等。所以CPU的存取顺序不一样,在网络上传输过去后,本来是“123456”,结果变成“563421”,这还只是数字,所以在网络上,我们需要统一一个字节序——网络字节序,然后发送的时候,由本机字节序转换成网络字节序,接受时由网络字节序转换为本机字节序,如果和网络字节序相同则不需要转换。当前约定网络字节序为大端序。不仅仅为了数据的一致性,也为了程序的可移植性

对于转换,就出现了下面的调用接口:

//这是针对端口的
//将主机字节序转换为网络字节序 
unit32_t htonl (unit32_t hostlong); 
unit16_t htons (unit16_t hostshort);
//h: host(本地), n:net(网络) l: long(长整型) s: short (短整型)

//将网络字节序转换为主机字节序 
unit32_t ntohl (unit32_t netlong); 
unit16_t ntohs (unit16_t netshort);

//这是针对IP地址的
in_addr_t inet_addr(const char *cp);
//将字符串点分十进制IP地址转换为网络字节序的整数IP地址
  • struct sockaddr

看名字就知道,给socket添加一个地址,但是不仅仅是添加地址,这个结构体是一个地址信息。现在给socket这个“文件”里面再添加这个地址信息,完善socket。看下面的调用接口:

#include //头文件

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

//返回值:成功返回 0 ,失败返回 -1

这个接口的第一个参数就是前面创建的套接字,第三个参数就是第二个参数这个结构体的大小。重点要说的就是这个结构体。这个结构体struct sockaddr主要是为了接口的统一。

在地址域中我们有AF_INET、AF_INET6、AF_UNIX,对于它们都有自己的结构体。我们一起看了吧。

struct sockaddr { 
    __SOCKADDR_COMMON (sa_);      /* Common data: address family and length.  */
     char sa_data[14];            /* Address data.  */ 
};



/* /usr/include/netinet/in.h */ 
/* Structure describing an Internet socket address.  */
//IPv4
struct sockaddr_in {
     __SOCKADDR_COMMON (sin_);
     in_port_t sin_port; /* Port number.  */ 
    struct in_addr sin_addr; /* Internet address.  */ 

/* Pad to size of `struct sockaddr'.  */
     unsigned char sin_zero[sizeof (struct sockaddr)
     - __SOCKADDR_COMMON_SIZE
     - sizeof (in_port_t)
     - sizeof (struct in_addr)];
 };


 /* /usr/include/netinet/in.h */ 
#ifndef __USE_KERNEL_IPV6_DEFS 
/* Ditto, for IPv6.  */

 struct sockaddr_in6 {
    __SOCKADDR_COMMON (sin6_);
    in_port_t sin6_port;             /* Transport layer port # */
    uint32_t sin6_flowinfo;          /* IPv6 flow information */
    struct in6_addr sin6_addr;       /* IPv6 address */
    uint32_t sin6_scope_id;          /* IPv6 scope-id */
 };

三个结构体中都有一个__SOCKADDR_COMMON,其实就是一个unsigned short int。

typedef unsigned short int sa_family_t;

#define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family

#define __SOCKADDR_COMMON_SIZE  (sizeof (unsigned short int))

下面对三个结构体画图。

Linux网络——套接字编程_第3张图片

由于接口要统一,所以第二个参数则是一个结构体指针,我们在使用的时候,则需要对我们的sockaddr_in等进行一个强转,但是对于sockaddr_in6它占的大小这么多,于是就有了第三个参数,我们需要传入结构体的大小,当sockaddr指针取到前两个字节就已经知道这是哪个地址信息了,因为前两个字节是地址域。具体怎么使用,我们在后面的代码上会有体现。

  • 客户端与服务端

首先我们要知道,对于网络通信,总是有一方先发起请求,另一方进行响应。于是我们把发起请求的一方称为客户端,响应的一方称为服务端。

在发起请求之前,服务端会告诉客户端,其IP地址和程序运行的端口。

UDP编程

  • 流程

有了上面的基础,下面我们把流程以及思路梳理清楚。对于UDP,它是无连接,不可靠的,所以它的流程较为简单。如图所示。

Linux网络——套接字编程_第4张图片

  • 接收与发送

在这里需要说明两个接口,由于UDP和TCP的数据接收和数据发送的两个接口不一样,因为UDP无连接的,而TCP是有连接的。所以这里分开来说。

#include 

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

返回值:成功返回接收的字节数,,失败返回 -1,,为 0 时对端关闭

 看名字就已经知道这是,发送数据,参数虽然多,但是一看就可以明白的。

socked:套接字

buf:要接收的数据

len:接收数据长度

flags:调用方式,有很多种方式,多个时可以用 |,但通常置 0, 代表阻塞接收

src_addr:发送端的地址信息,这是输出型参数

addrlen:地址信息结构体的长度,输入型的参数,也就是说我们要接收多长的地址信息

#include 

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

返回值:成功返回发送的字节数,失败返回 -1

socked:套接字

buf:要发送的数据

len:数据长度

flags:调用方式,通常置 0

src_addr:接收端的地址信息,这是输入型参数

addrlen:本端地址信息结构体的长度,输入型的参数

  • 实战演练

首先我们对socket API进行封装,方便我们调用。

  • udpsocket.h
#include
#include//创建套接字的头文件
#include//地址信息结构体、IP字节序转换头文件
#include//端口号字节序转换
#include
#include
#include//close的头文件
using namespace std;

#define CHECK_RET(q) if((q) == false){return -1;}
class UdpSocket
{
	public:
		UdpSocket()
			:socket_(-1)
		{
			//创建套接字不放在构造中,因为创建失败后无法处理
		}
		bool CreateSocket()
		{

			socket_ = socket(AF_INET,SOCK_DGRAM,0);
			if(socket_ < 0)
			{
				cout << "create error" << endl;
				return false;
			}
			return true;
		}
		bool Bind(string& ip, uint16_t& port)
		{
			struct sockaddr_in addr;
			addr.sin_family = AF_INET;
			addr.sin_port = htons(port);//这里就需要网络字节序
			addr.sin_addr.s_addr = inet_addr(ip.c_str());//同上
			socklen_t len = sizeof(addr);
			int ret = bind(socket_,(sockaddr*)&addr,len);
			if(ret < 0)
			{
				cout << "bind error" << endl;
				return false;
			}
			return true;
		}
		bool Recv(string& retmsg, struct sockaddr_in* cliaddr = NULL)
		{//如果用户没有给第二个参数则不管,是缺省的,如果给了,则返回发送端的地址
                    
			char buff[1024];
			struct sockaddr_in addr;
			socklen_t len = sizeof(addr);
			int ret = recvfrom(socket_,buff,1023,0,
                            (struct sockaddr*)&addr,&len);
			if(ret < 0)
			{
				cout << "recv error" << endl;
				return false;
			}
			if(ret == 0)
			{
				cout << "peer down" << endl;
				return false;
			}
			retmsg.assign(buff);
			if(cliaddr != NULL)
			{
				memcpy(cliaddr,&addr,len);
			}
			return true;
		}
		bool Send(string& buff, struct sockaddr_in& destaddr)
		{
			socklen_t len = sizeof(destaddr);
			int ret = sendto(socket_,buff.c_str(),buff.size()
                            ,0,(struct sockaddr*)&destaddr,len);

			if(ret < 0)
			{
				cout << "sned error" << endl;
				return false;
			}
			return true;
		}
		bool Close()
		{
			close(socket_);
			socket_ = -1;
			return true;
		}
	private:
		int socket_;
};
  • 服务端server.cpp
#include
#include
#include"udpsocket.h"
using namespace std;

int main()
{
	string ip = "192.168.138.135";
	uint16_t port = 9000;
	UdpSocket sock;
	CHECK_RET(sock.CreateSocket());
	CHECK_RET(sock.Bind(ip,port));
	while(1)
	{
		struct sockaddr_in client;//创建一个IPv4的结构体,用来获取客户端地址
		string buff;
		sock.Recv(buff,&client);
		cout << "client say: " << buff << endl;

		cout << "server.say: ";
		fflush(stdout);
		cin >> buff;
		sock.Send(buff,client);//有了客户端地址,就可以发送了
	}
	sock.Close();
	return 0;
}
  • 客户端 client.cpp
#include
#include
#include"udpsocket.h"
using namespace std;


int main()
{
	string ip = "192.168.138.135";
	UdpSocket client;
	CHECK_RET(client.CreateSocket());
	struct sockaddr_in seraddr;    //服务端的地址信息,客户端没有绑定地址的
	seraddr.sin_family = AF_INET;
	seraddr.sin_port = htons(9000);
	seraddr.sin_addr.s_addr = inet_addr(ip.c_str());
	while(1)
	{
		string msg;
		cout << "client say: ";
		fflush(stdout);
		cin >> msg;
		client.Send(msg,seraddr);
		
		client.Recv(msg);
		cout << "server say: " << msg << endl;
	}
	client.Close();
	return 0;
}

TCP编程

  • 流程

对比UDO,TCP的流程就要多加三步操作了——开始监听、发起请求、建立连接

Linux网络——套接字编程_第5张图片

  • 建立连接

建立连接的过程,对比三次握手。这里有三个过程就是上面的三个过程。分别是下面的接口。

  • 开始监听
#include 

int listen(int s, int backlog);

//返回值:成功返回 0,失败返回 -1

 第一个参数是套接字,socket,第二个参数则指定了已经完成连接正等待应用程序接收的套接字队列的长度。也就是说,在内核中有两个队列。

Linux网络——套接字编程_第6张图片

  • 发起请求
#include 

int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

//返回值:成功返回 0,失败返回 -1

这里参数和UDP是一样的。socked:套接字,addr:接收端地址信息,addrlen:地址信息长度

  • 建立连接
#include 

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

//返回值:成功返回与客户端通信的新套接字,失败返回 -1

addr:  用于存储客户端地址信息 

addrlen:用于设置想要的地址长度和保存实际的地址长度

  • 接收与发送

这里接收与发送就比较简单了,因为已经建立了连接,所以相对简单许多

  • 接收
#include 

//这里是recv,不再是recvfrom
ssize_t recv(int sockfd, void *buf, size_t len, int flags)

//返回值:实际接收的字节数/对端关闭连接返回0    错误:-1

sockfd:   套接字描述符
buf:     存储接收的数据
len:     想要接收的长度
flags:    0-阻塞接收   
MSG_PEEK-接收数据但是数据不从接收缓冲区移除 

  • 发送
#include 

//是send,不是sendto
int send(int s, const void *msg, size_t len, int flags);
//返回值:实际发送的字节数      失败:-1
//若连接已经断开,发送会触发异常
  • 实战演练

还是一样,先封装。

  • tcpsocket.h
#include
#include
#include
#include
#include
#include
#include //htons的头文件
#include
using namespace std;
#define CHECK_RET(q) if((q) == false){ return -1; }
class TcpSocket
{
	public:
		bool CreateSocket()
		{
			socket_ = socket(AF_INET, SOCK_STREAM, 6);
			if(socket_ < 0)
			{
				cout << "create error!" << endl;
				return false; 
			}
			return true;
		}
		bool Listen(int lisMax = 5)//开始监听,设置服务端的同一时间最大并发连接数
		{
			//int listen(int sockfd, int backlog);
			//sockfd:   套接字描述符
			//backlog: backlog设置内核中已完成连接队列的最大节点数量
			int ret = listen(socket_,lisMax);
			if(ret < 0)
			{
				cout << "listen error" << endl;
				return false;
			}
			return true;
		}
		bool SetFd(int fd)
		{
			socket_ = fd;
			return true;
		}
		bool Accept(TcpSocket& socket,struct sockaddr_in *addr = NULL)
		{	
    		        sockaddr_in add;
			socklen_t len = sizeof(sockaddr_in);
			int ret = accept(socket_,(sockaddr*)&add, &len);
                        //接下来与客户端的通信都是通过这个socket描述符实现的        
			if(ret < 0)
			{
				cout << "accept error" << endl;
				return false;
			}
			socket.SetFd(ret);
			if(addr)
			{
				memcpy(addr, &add, len);
			}
			return true;
		}
		////客户端向服务端发起连接请求
		bool Connect(string& ip, uint16_t port)//建立连接
		{

			struct sockaddr_in addr;
			addr.sin_family = AF_INET;
			addr.sin_addr.s_addr = inet_addr(ip.c_str());
			addr.sin_port = htons(port);
			socklen_t len = sizeof(addr);
			int ret = connect(socket_,(sockaddr*)&addr,len);
			if(ret < 0)
			{
				cout << "connect error" << endl;
				return false;
			}
			return true;
		}
		bool Bind(string& ip,uint16_t&  port)
		{

			struct sockaddr_in  addr;
			addr.sin_family = AF_INET;

			addr.sin_port = htons(port);
			addr.sin_addr.s_addr = inet_addr(ip.c_str());

			socklen_t len = sizeof(addr);
			int ret = bind(socket_,(sockaddr*)&addr,len);
			if(ret < 0)
			{
				cout << "bind error" << endl;
				return false;
			}
			return true;
		}
		bool Recv(string& buff)
		{	
			char buf[1024] = {0};
			int ret = recv(socket_, buf, 1023, 0);
			if(ret < 0)
			{
				cout << "recive error!" << endl;
				return false;
			}
			else if(ret == 0)
			{
				cout << "peer down" << endl;
				return false;
			}
			buff.assign(buf);
			return true;
		}
		bool Send(string& buff)
		{
			int ret = send(socket_, buff.c_str(), buff.size(), 0);
			if(ret < 0)
			{
				cout << "send error" << endl;
				return false;
			}
			return true;
		}
		bool Close()
		{
			close(socket_);
			socket_ = -1;
			return true;
		}
	private:
		int socket_ = -1;
};
  • server.cpp
#include
#include
#include "tcpsocket.h"
using namespace std;
void* func(void* arg)
{
	TcpSocket* clisocket = ((TcpSocket*)arg);
	while(1)
	{
		string buff;
		clisocket->Recv(buff);
		cout << "client say:" << buff << endl;
		buff.clear();
		cout << "server say:";
		fflush(stdout);
		cin >> buff;
		clisocket->Send(buff);
	}
	clisocket->Close();
	return NULL;
}
int main()
{
	string ip("192.168.138.135");
	uint16_t port_ = 9000;
	TcpSocket socket;
	CHECK_RET(socket.CreateSocket());
	CHECK_RET(socket.Bind(ip,port_));
	CHECK_RET(socket.Listen());
	while(1)
	{
		TcpSocket* clisocket = new TcpSocket();
		if(socket.Accept(*clisocket) == false)
		{
			cout << "someone clienr accept error" << endl;
			continue;
                //一个客户端的失败,我们不能直接return false,因为还有其他客户端
		}
		pthread_t thread;
                //使用线程,可以和多个客户端通信
		pthread_create(&thread,NULL,func,(void*)clisocket);
		pthread_detach(thread);
	}
	socket.Close();
	return 0;	
}
  • client.cpp
#include
#include"tcpsocket.h"
using namespace std;
int main()
{
	TcpSocket client;	
	CHECK_RET(client.CreateSocket());
	string ip("192.168.138.135");
	uint16_t port = 9000;
	CHECK_RET(client.Connect(ip,port));
	while(1)
	{
		string buff;
		cout << "client say: ";
		fflush(stdout);
		cin >> buff;
		client.Send(buff);
		buff.clear();
		client.Recv(buff);
		cout << "server say: " << buff << endl;

	}
	client.Close();
	return 0;
}

有了准备工作和UDP的练习,畅快了很多。

HTTP应用

  • HTTP服务器

既然有了TCP,就来实现一个简单的HTTP服务器,这样对HTTP协议会有更进一步的认知。

#include
#include"tcpsocket.h"
#include
using namespace std;
int main()
{
	int i = 0;
	TcpSocket sersocket;
	string ip = "192.168.138.135";
	uint16_t port = 9000;
	CHECK_RET(sersocket.CreateSocket());
	CHECK_RET(sersocket.Bind(ip,port));
	CHECK_RET(sersocket.Listen());
	while(1)
	{
		TcpSocket client;
		if(sersocket.Accept(client) == false)
		{
			cout << "Accept Error" << endl;
			continue;
		}
		string recv;
		client.Recv(recv);//在服务器上,我们显示HTTP请求
		cout << "client[" << i++ << "]" << endl;
		cout << recv << endl;
		string resp,bodystr;
		const char* body = "

hello world

"; char buff[1024]; sprintf(buff,"HTTP/1.1 200 OK\nContent-Length:%lu\n\n%s",strlen(body),body); bodystr += buff; client.Send(bodystr);//根据HTTP协议发送数据 client.Close(); } sersocket.Close(); return 0; }

在虚拟机的Linux上,可能会访问失败,因为防火墙。根据下面的命令可以关闭和开启防火墙

关闭防火墙的命令:systemctl stop firewalld.service                                                               
启动防火墙的命令:systemctl start firewalld.service

 

  •  客户端则会出现下面的界面。

Linux网络——套接字编程_第7张图片

 

  • 服务端则是

Linux网络——套接字编程_第8张图片

结束

OK,到这里网络的学习就告一段落了,如果有小伙伴们读到这,衷心感谢您的耐心了。这一系列比较仓促,如果有不足或者错误的地方欢迎指正。

 

你可能感兴趣的:(Linux网络编程系列)