Linux网络编程套接字(中)

Posted 一起去看日落吗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux网络编程套接字(中)相关的知识,希望对你有一定的参考价值。

🎇Linux:


  • 博客主页:一起去看日落吗
  • 分享博主的在Linux中学习到的知识和遇到的问题
  • 博主的能力有限,出现错误希望大家不吝赐教
  • 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐🌿🌞🐾。

✨ ⭐️ 🌟 💫


目录

🌟1. 简单的TCP网络程序

⭐️1.1 服务端创建套接字

我们将TCP服务器封装成一个类,当我们定义出一个服务器对象后需要马上对服务器进行初始化,而初始化TCP服务器要做的第一件事就是创建套接字。

TCP服务器在调用socket函数创建套接字时:

  • 协议家族选择AF_INET,因为我们要进行的是网络通信。
  • 创建套接字时所需的服务类型应该是SOCK_STREAM,因为我们编写的是TCP服务器,SOCK_STREAM提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务。
  • 协议类型默认设置为0即可。

如果创建套接字后获得的文件描述符是小于0的,说明套接字创建失败,此时也就没必要进行后续操作了,直接终止程序即可。

class TcpServer

public:
	void InitServer()
	
		//创建套接字
		_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_sock < 0)
			std::cerr << "socket error" << std::endl;
			exit(2);
		
	
	~TcpServer()
	
		if (_sock >= 0)
			close(_sock);
		
	
private:
	int _sock; //套接字
;

说明一下:

  • 实际TCP服务器创建套接字的做法与UDP服务器是一样的,只不过创建套接字时TCP需要的是流式服务,而UDP需要的是用户数据报服务。
  • 当析构服务器时,可以将服务器对应的文件描述符进行关闭。

⭐️1.2 服务端绑定

套接字创建完毕后我们实际只是在系统层面上打开了一个文件,该文件还没有与网络关联起来,因此创建完套接字后我们还需要调用bind函数进行绑定操作。

绑定的步骤如下:

  • 定义一个struct sockaddr_in结构体,将服务器网络相关的属性信息填充到该结构体当中,比如协议家族、IP地址、端口号等。
  • 填充服务器网络相关的属性信息时,协议家族对应就是AF_INET,端口号就是当前TCP服务器程序的端口号。在设置端口号时,需要调用htons函数将端口号由主机序列转为网络序列。
  • 在设置服务器的IP地址时,我们可以设置为本地环回127.0.0.1,表示本地通信。也可以设置为公网IP地址,表示网络通信。
  • 如果使用的是云服务器,那么在设置服务器的IP地址时,不需要显示绑定IP地址,直接将IP地址设置为INADDR_ANY即可,此时服务器就可以从本地任何一张网卡当中读取数据。此外,由于INADDR_ANY本质就是0,因此在设置时不需要进行网络字节序的转换。
  • 填充完服务器网络相关的属性信息后,需要调用bind函数进行绑定。绑定实际就是将文件与网络关联起来,如果绑定失败也没必要进行后续操作了,直接终止程序即可。

由于TCP服务器初始化时需要服务器的端口号,因此在服务器类当中需要引入端口号,当实例化服务器对象时就需要给传入一个端口号。而由于我当前使用的是云服务器,因此在绑定TCP服务器的IP地址时不需要绑定公网IP地址,直接绑定INADDR_ANY即可,因此我这里没有在服务器类当中引入IP地址。

class TcpServer

public:
	TcpServer(int port)
		: _sock(-1)
		, _port(port)
	
	void InitServer()
	
		//创建套接字
		_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_sock < 0)
			std::cerr << "socket error" << std::endl;
			exit(2);
		
		//绑定
		struct sockaddr_in local;
		memset(&local, '\\0', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;
		
		if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
			std::cerr << "bind error" << std::endl;
			exit(3);
		
	
	~TcpServer()
	
		if (_sock >= 0)
			close(_sock);
		
	
private:
	int _sock; //监听套接字
	int _port; //端口号
;

当定义好struct sockaddr_in结构体后,最好先用memset函数对该结构体进行清空,也可以用bzero函数进行清空。bzero函数也可以对特定的一块内存区域进行清空。

void bzero(void *s, size_t n);

TCP服务器绑定时的步骤与UDP服务器是完全一样的,没有任何区别。


⭐️1.3 服务端监听

UDP服务器的初始化操作只有两步,第一步就是创建套接字,第二步就是绑定。而TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信。

因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态。

  • listen函数
int listen(int sockfd, int backlog);

参数说明:

  • sockfd:需要设置为监听状态的套接字对应的文件描述符。
  • backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。

返回值说明:

  • 监听成功返回0,监听失败返回-1,同时错误码会被设置。

  • 服务器监听

TCP服务器在创建完套接字和绑定后,需要再进一步将套接字设置为监听状态,监听是否有新的连接到来。如果监听失败也没必要进行后续操作了,因为监听失败也就意味着TCP服务器无法接收客户端发来的连接请求,因此监听失败我们直接终止程序即可

#define BACKLOG 5

class TcpServer

public:
	void InitServer()
	
		//创建套接字
		_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_listen_sock < 0)
			std::cerr << "socket error" << std::endl;
			exit(2);
		
		//绑定
		struct sockaddr_in local;
		memset(&local, '\\0', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;
		
		if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
			std::cerr << "bind error" << std::endl;
			exit(3);
		
		//监听
		if (listen(_listen_sock, BACKLOG) < 0)
			std::cerr << "listen error" << std::endl;
			exit(4);
		
	
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
;

说明一下:

  • 初始化TCP服务器时创建的套接字并不是普通的套接字,而应该叫做监听套接字。为了表明寓意,我们将代码中套接字的名字由sock改为listensocket。
  • 在初始化TCP服务器时,只有创建套接字成功、绑定成功、监听成功,此时TCP服务器的初始化才算完成。

⭐️1.4 服务端获取连接

TCP服务器初始化后就可以开始运行了,但TCP服务器在与客户端进行网络通信之前,服务器需要先获取到客户端的连接请求。

  • accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明:

  • sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。

返回值说明:

  • 获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。

  • accept函数返回的套接字是什么?

调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。

监听套接字与accept函数返回的套接字的作用:

  1. 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
  2. accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。

  • 服务端获取连接

  • accept函数获取连接时可能会失败,但TCP服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续获取连接。

  • 如果要将获取到的连接对应客户端的IP地址和端口号信息进行输出,需要调用inet_ntoa函数将整数IP转换成字符串IP,调用ntohs函数将端口号由网络序列转换成主机序列。

  • inet_ntoa函数在底层实际做了两个工作,一是将网络序列转换成主机序列,二是将主机序列的整数IP转换成字符串风格的点分十进制的IP。

class TcpServer

public:
	void Start()
	
		for (;;)
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '\\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0)
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout<<"get a new link->"<<sock<<" ["<<client_ip<<"]:"<<client_port<<std::endl;
		
	
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
;


服务端运行后,通过netstat命令可以查看到一个程序名为tcp_server的服务程序,它绑定的端口就是8081,而由于服务器绑定的是INADDR_ANY,因此该服务器的本地IP地址是0.0.0.0,这就意味着该TCP服务器可以读取本地任何一张网卡里面的数据。此外,最重要的是当前该服务器所处的状态是LISTEN状态,表明当前服务器可以接收外部的请求连接。

虽然现在还没有编写客户端相关的代码,但是我们可以使用telnet命令远程登录到该服务器,因为telnet底层实际采用的就是TCP协议。

使用telnet命令连接当前TCP服务器后可以看到,此时服务器接收到了一个连接,为该连接提供服务的套接字对应的文件描述符就是4。因为0、1、2是默认打开的,其分别对应标准输入流、标准输出流和标准错误流,而3号文件描述符在初始化服务器时分配给了监听套接字,因此当第一个客户端发起连接请求时,为该客户端提供服务的套接字对应的文件描述符就是4。


如果此时我们再用其他窗口继续使用telnet命令,向该TCP服务器发起请求连接,此时为该客户端提供服务的套接字对应的文件描述符就是5。


⭐️1.5 服务端处理请求

现在TCP服务器已经能够获取连接请求了,下面当然就是要对获取到的连接进行处理。但此时为客户端提供服务的不是监听套接字,因为监听套接字获取到一个连接后会继续获取下一个请求连接,为对应客户端提供服务的套接字实际是accept函数返回的套接字,下面就将其称为“服务套接字”。

为了让通信双方都能看到对应的现象,我们这里就实现一个简单的回声TCP服务器,服务端在为客户端提供服务时就简单的将客户端发来的数据进行输出,并且将客户端发来的数据重新发回给客户端即可。当客户端拿到服务端的响应数据后再将该数据进行打印输出,此时就能确保服务端和客户端能够正常通信了。

  • read函数
ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • count:数据的个数,表示从该文件描述符中读取数据的字节数。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。

  • read返回值为0表示对端连接关闭

这实际和本地进程间通信中的管道通信是类似的,当使用管道进行通信时,可能会出现如下情况:

  1. 写端进程不写,读端进程一直读,此时读端进程就会被挂起,因为此时数据没有就绪。
  2. 读端进程不读,写端进程一直写,此时当管道被写满后写端进程就会被挂起,因为此时空间没有就绪。
  3. 写端进程将数据写完后将写端关闭,此时当读端进程将管道当中的数据读完后就会读到0。
  4. 读端进程将读端关闭,此时写端进程就会被操作系统杀掉,因为此时写端进程写入的数据不会被读取。

这里的写端就对应客户端,如果客户端将连接关闭了,那么此时服务端将套接字当中的信息读完后就会读取到0,因此如果服务端调用read函数后得到的返回值为0,此时服务端就不必再为该客户端提供服务了。


  • write函数
ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要写入的数据。
  • count:需要写入数据的字节个数。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

当服务端调用read函数收到客户端的数据后,就可以再调用write函数将该数据再响应给客户端。


  • 服务端处理请求

需要注意的是,服务端读取数据是服务套接字中读取的,而写入数据的时候也是写入进服务套接字的。也就是说这里为客户端提供服务的套接字,既可以读取数据也可以写入数据,这就是TCP全双工的通信的体现。

在从服务套接字中读取客户端发来的数据时,如果调用read函数后得到的返回值为0,或者读取出错了,此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标,因此文件描述符的资源是有限的,如果我们一直占用,那么可用的文件描述符就会越来越少,因此服务完客户端后要及时关闭对应的文件描述符,否则会导致文件描述符泄漏。

class TcpServer

public:
	void Service(int sock, std::string client_ip, int client_port)
	
		char buffer[1024];
		while (true)
			ssize_t size = read(sock, buffer, sizeof(buffer)-1);
			if (size > 0) //读取成功
				buffer[size] = '\\0';
				std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;

				write(sock, buffer, size);
			
			else if (size == 0) //对端关闭连接
				std::cout << client_ip << ":" << client_port << " close!" << std::endl;
				break;
			
			else //读取失败
				std::cerr << sock << " read error!" << std::endl;
				break;
			
		
		close(sock); //归还文件描述符
		std::cout << client_ip << ":" << client_port << " service done!" << std::endl;
	
	void Start()
	
		for (;;)
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '\\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0)
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link [" << client_ip << "]:" << client_port << std::endl;

			//处理请求
			Service(sock, client_ip, client_port);
		
	
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
;

⭐️1.6 客户端创建套接字

同样的,我们将客户端也封装成一个类,当我们定义出一个客户端对象后也需要对其进行初始化,而初始化客户端唯一需要做的就是创建套接字。而客户端在调用socket函数创建套接字时,参数设置与服务端创建套接字时是一样的。

客户端不需要进行绑定和监听:

  • 服务端要进行绑定是因为服务端的IP地址和端口号必须要众所周知,不能随意改变。而客户端虽然也需要IP地址和端口号,但是客户端并不需要我们进行绑定操作,客户端连接服务端时系统会自动指定一个端口号给客户端。
  • 服务端需要进行监听是因为服务端需要通过监听来获取新连接,但是不会有人主动连接客户端,因此客户端是不需要进行监听操作的。

此外,客户端必须要知道它要连接的服务端的IP地址和端口号,因此客户端除了要有自己的套接字之外,还需要知道服务端的IP地址和端口号,这样客户端才能够通过套接字向指定服务器进行通信。

class TcpClient

public:
	TcpClient(std::string server_ip, int server_port)
		: _sock(-1)
		, _server_ip(server_ip)
		, _server_port(server_port)
	
	void InitClient()
	
		//创建套接字
		_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_sock < 0)
			std::cerr << "socket error" << std::endl;
			exit(2);
		
	
	~TcpClient()
	
		if (_sock >= 0)
			close(_sock);
		
	
private:
	int _sock; //套接字
	std::string _server_ip; //服务端IP地址
	int _server_port; //服务端端口号
;

⭐️1.7 客户端连接服务器

由于客户端不需要绑定,也不需要监听,因此当客户端创建完套接字后就可以向服务端发起连接请求。

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

参数说明:

  • sockfd:特定的套接字,表示通过该套接字发起连接请求。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

  • 连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。

  • 客户端连接服务器

需要注意的是,客户端不是不需要进行绑定,而是不需要我们自己进行绑定操作,当客户端向服务端发起连接请求时,系统会给客户端随机指定一个端口号进行绑定。因为通信双方都必须要有IP地址和端口号,否则无法唯一标识通信双方。也就是说,如果connect函数调用成功了,客户端本地会随机给该客户端绑定一个端口号发送给对端服务器。

此外,调用connect函数向服务端发起连接请求时,需要传入服务端对应的网络信息,否则connect函数也不知道该客户端到底是要向哪一个服务端发起连接请求。

class TcpClient

public:
	void Start()
	
		struct sockaddr_in peer;
		memset(&peer, '\\0', sizeof(peer));
		peer.sin_family = AF_INET;
		peer.sin_port = htons(_server_port);
		peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
		
		if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0) //connect success
			std::cout << "connect success..." << std::endl;
			Request(); //发起请求
		
		else //connect error
			std::cerr << "connect failed..." << std::endl;
			exit(3);
		
	
private:
	int _sock; //套接字
	std::string _server_ip; //服务端IP地址
	int _server_port; //服务端端口号
;

⭐️1.8 客户端发起请求

由于我们实现的是一个简单的回声服务器,因此当客户端连接到服务端后,客户端就可以向服务端发送数据了,这里我们可以让客户端将用户输入的数据发送给服务端,发送时调用write函数向套接字当中写入数据即可。

当客户端将数据发送给服务端后,由于服务端读取到数据后还会进行回显,因此客户端在发送数据后还需要调用read函数读取服务端的响应数据,然后将该响应数据进行打印,以确定双方通信无误。

class TcpClient

public:
	void Request()
	
		std::string msg;
		char buffer[1024];
		while (true)
			std::cout << "Please Enter# ";
			getline(std::cin, msg);

			write(_sock, msg.c_str(), msg.size());

			ssize_t size = read(_sock, buffer, sizeof(buffer)-1);
			if (size > 0)
				buffer[size] = '\\0';
				std::cout << "server echo# " << buffer << std::endl;
			
			else if (size == 0)
				std::cout << "server close!" << std1、套接字概述 

套接字的本意是插座,在网络中用来描述计算机中不同程序与其他计算机程序的通信方式。
常用的套接字类型有3种:
1)流套接字(SOCK——STREAM):使用了面向连接的可靠的数据通信方式,即TCP套接字
2)数据报套接字(Raw Sockets):使用了不面向连接的数据传输方式,即UDP套接字
3)原始套接字(SOCK——RAW):没有经过处理的IP数据包,可以根据自己程序的要求进行封装。

2、常用函数

1、创建套接字函数:成功时返回文件描述符,失败时返回-1

int socket(int domain,int type,int protocol);
//参数domain用于指定创建套接字所使用的协议族(可取AF_UNIX,AF_INET,AF_INTE6)
//参数type指定套接字的类型(可取SOCK_STREAM,SOCK_DGRAM,SOCK_RAW)
//参数protocol通常设置为0

2、在指定套接字上创建链接函数:成功时返回0,失败时返回-1

int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen);
//参数sockfd是一个由函数socket创建的套接字
//参数serv_addr是一个地址结构,指定服务器的IP地址和端口号
//参数addrlen为参数serv_addr的长度

3、将一个套接字和某个端口绑定在一起的函数:成功时返回0,失败时返回-1

int bind(int sockfd,struct sockaddr *my_addr,socklen_t addrlen);
//一般只有服务器端的程序调用,参数my_addr指定了sockfd将绑定到的本地
//地址,可以将参数my_addr的sin_addr设置为INADDR_ANY而不是某个确定
//IP地址就可以绑定到任何网络接口。

4、把套接字转化为被动监听函数:成功时返回0,失败时返回-1

int listen(int s,int backlog);
//参数s为套接字,参数backlog指定链接请求队列的最大长度;

5、接收连接请求函数:成功时返回文件描述符,失败时返回-1

int accept(int s,struct sockaddr *addr,socklen_t *addrlen);
//参数s是由函数socket创建,经函数bind绑定到本地某一端口上,然后通过
//函数listen转化而来的监听套接字
//参数addr用来保存发起连接请求的主机的地址和端口
//参数addrlen是addr所指向的结构体的大小

6、在TCP套接字上发送数据函数:有连接

包含3要素:套接字s,待发数据msg,数据长度len

ssize_t send(int s,const void *msg,size_t len,int flags);
//函数只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述
//符,即accept函数的返回值
//参数msg指向存放待发送数据的缓冲区
//参数len为待发送数据的长度,参数flags为控制选项,一般设置为0

7、在TCP套接字上接收数据函数:有连接

包含3要素:套接字s,接收缓冲区buf,长度len

ssize_t recv(int s,void *buf,size_t len,int flags);
//函数recv从参数s所指定的套接字描述符(必须是面向连接的套接字)上接收
//数据并保存到参数buf所指定的缓冲区
//参数len则为缓冲区长度,参数flags为控制选项,一般设置为0

8、在UCP套接字上发送数据函数:无连接

ssize_t sendto(int s,const void *msg,size_t len,int flags,const struct sockaddr *to,socklen_t tolen);
//函数功能与函数send类似,但函数sendto不需要套接字处于连接状态,所以
//该函数通常用来发送UDP数据,同时因为是无连接的套接字,在使用sendto时
//需要指定数据的目的地址,参数msg指向待发送数据的缓冲区。
//参数len指定了待发送数据的长度
//参数flags是控制选项,含义与send函数中的一致
//参数to用于指定目的地址,目的地址的长度由tolen指定

9、在UDP套接字上接收数据函数:无连接

ssize_t recvfrom(int s ,void *buf,size_t len,int flags,struct sockaddr *from,socklen_t *fromlen);
//与函数recv功能类似,只是函数recv只能用于面向连接的套接字,而函数
//recvfrom没有此限制,可以用于从无连接的套接字上接收数据
//参数buf指向接收缓冲区
//参数len指定了缓冲区的大小
//参数flags是控制选项,含义与recv中的一致
//如果参数from非空,且该套接字不是面向连接的,则函数recvfrom返回时,
//参数from中将保存数据的源地址
//参数fromlen在调用recvfrom前为参数from的长度,调用recvfrom后将
//保存from的实际大小

10、关闭套接字函数:

int close(int fd);
//参数fd为一个套接字描述符;

11、多路复用函数:

int select(int n,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
//参数n是需要监视的文件描述符数
//参数readfds指定需要监视的可读文件描述符集合
//参数writefds指定需要监视的可写文件描述符集合
//参数exceptfds指定需要监视的异常文件描述符的集合
//参数timeout指定了阻塞的时间

3、服务器端套接字(接收连接请求的套接字)创建过程

第一步:调用socket函数创建套接字。
第二步:调用bind函数分配IP地址和端口号。
第三部:调用listen函数转为可接收请求状态。
第四步:调用accept函数受理连接请求。

另外还有read/write,以及close。

4、客户端套接字(发送连接请求的套接字)创建过程

只有两步:
1、调用socket函数创建套接字。
2、调用connect函数向服务器端发送连接请求。

另外还有read/write,以及close。

5、Linux的文件操作

文件描述符:是系统自动分配给文件或套接字的整数。

每当生成文件或套接字,操作系统就会自动返回给我们一个整数。这个整数就是文件描述符,即创建的文件或套接字的别名,方便称呼而已。文件描述符在Windows中又称为句柄。

1、打开文件:
这里写图片描述

2、关闭文件或套接字:
这里写图片描述

3、将数据写入文件:
这里写图片描述

4、读取文件中的数据:
这里写图片描述

注:ssize_t = signed int, size_t = unsigned int,都是通过typedef声明,为基本数据类型取的别名。既然已经有了基本数据类型,那么为什么还需要为它取别名呢?是因为目前普遍认为int是32位的,而过去16位操作系统时代,int是16位的。根据系统的不同,时代的变化,基本数据类型的表现形式也随着变化的如果为基本数据类型取了别名,以后要修改,也就只需要修改typedef声明即可,这将大大减少代码变动。

6、服务器端实例

服务器端受理连接请求的程序。编译并运行该程序,创建等待连接请求的服务器端。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc, const char * argv[]) 
{
    int serv_sock;
    int clnt_sock;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char message[] = "Hello World!";

    if(argc != 2)
    {
        printf("Usage:%s <port>\\n", argv[0]);
        exit(1);
    }
    //(1)调用socket函数创建套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock == -1)
        error_handling("socket() error");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));
    //(2)调用bind函数分配IP地址和端口号
    if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error");
    //(3)调用listen函数转为可接收请求状态
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    clnt_addr_size = sizeof(clnt_addr);
    //(4)调用accept函数受理连接请求.没有连接请求时调用不会返回,直到有请求
    clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
    if(clnt_sock == -1)
        error_handling("accept() error");
    //(5)write函数用于传输数据。执行到此行说明已有了连接请求
    //向文件描述符为clnt_sock的客户端文件中传输message中的数据
    write(clnt_sock, message, sizeof(message));
    close(clnt_sock);//(6)
    close(serv_sock);//(7)

    return 0;
}
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\\n', stderr);
    exit(1);
}

7、客户端实例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc, const char * argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len;

    if(argc != 3)
    {
        printf("Usage: %s <IP> <port>\\n", argv[0]);
        exit(1);
    }
    //(1)创建套接字.但此时并不马上分为客户端或服务器端
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));
    //(2)调用connect函数向服务器端发送连接请求.此时确定为客户端
    if(connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error");
    //(3)调用read函数向自身声明的message数组中保存数据
    str_len = read(sock, message, sizeof(message) - 1);
    if(str_len == -1)
        error_handling("read() error");

    printf("Message from server: %s \\n", message);
    close(sock);//(4)

    return 0;
}
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\\n', stderr);
    exit(1);
}

7、基本客户/服务器套接字图形展示

这里写图片描述

参考:《TCP/IP网络编程》及《Unix网络编程卷1》

以上是关于Linux网络编程套接字(中)的主要内容,如果未能解决你的问题,请参考以下文章

Linux网络编程和套接字

Linux之网络管理网络监控工具

Linux/UNIX网络编程的目录

Linux——网络编程套接字

Linux网络编程套接字(中)

linux网络编程——套接字(socket)入门