SOCKET(一)基本TCP套接字编程(socket,bind,listen,accept,connect函数)

概述

TCP套接字的编程流程/函数框架:
在这里插入图片描述
TCP套接字程序编写流程:

  • 创建套接字——socket
  • 将套接字与地址和端口捆绑

socket函数

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

功能:创建套接字,指定其通信协议类型和套接字类型,返回一个套接字描述符(int 类似于文件描述符)
1:domain
指定协议族,man中的可选范围如下:常用的有AF_UNIX(用于通过文件系统实现的本地套接字,类似于pipe管道)、AF_INET(ipv4协议族)、AF_INET6(ipv6协议族)

       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)
       AF_IPX              IPX - Novell protocols
       AF_NETLINK          Kernel user interface device     netlink(7)
       AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
       AF_AX25             Amateur radio AX.25 protocol
       AF_ATMPVC           Access to raw ATM PVCs
       AF_APPLETALK        AppleTalk                        ddp(7)
       AF_PACKET           Low level packet interface       packet(7)
       AF_ALG              Interface to kernel crypto API

2:type
指定套接字的类型:(仅部分,全部可用见 man socket)

   SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mechanism may be supported.
字节流套接字,基于 TCP,提供可靠,有序的服务。
   SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
数据报套接字,基于 UDP,提供不可靠,无序的服务。
   SOCK_SEQPACKET  Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is  required
                   to read an entire packet with each input system call.
有序分组套接字
   SOCK_RAW        Provides raw network protocol access.
   原始套接字

3:protocol
为套接字特别指定一种要用的传输协议。在给定的协议族中,通常只有一个协议支持特定的套接字类型,在这种情况下,协议可以指定为0。然而,可能存在许多协议,在这种情况下,必须以protocol这种方式指定特定的协议。
protocol应该设置为下图所示类型或者为0(选择给定domain和type组合的默认值)
(UNPp79)
在这里插入图片描述
并非所有的family和type的组合都是有效的,常见的组合如下表
在这里插入图片描述
栗子(TCP):

	int server_socket_fd;
	server_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (server_socket_fd < 0)
	{
		perror("create socket err:");
		return -1;
	}

bind函数(服务端)

socket函数执行后,我们只是得到了这个套接字描述符,并指定了套接字的协议族和套接字类型,并没有指定本地协议地址或者远程协议地址,这就是下面的函数的工作。
功能:
把一个本地协议地址赋予一个套接字
bind有将地址和端口与套接字“绑定”的意思
对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,或者都指定,或都不指定
返回值:
成功返回0,失败返回-1

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

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

1:sockfd
前面socket函数返回的,与套接字关联的文件描述符
2:addr
这是一个指向保存指定地址结构体的指针
bind函数的功能就是将addr指向结构体中的地址分配给sockfd关联的套接字,至于结构体addr,不同的address family有不同的定义,其长度不一,addr结构体的长度由第三参数addrlen传递

The actual structure passed for the addr argument will depend on the address family.  The sockaddr structure is defined as something like:

 struct sockaddr {
     sa_family_t sa_family;//地址族
     char        sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
 }


然而,在实际编码中,我们通常使用sockaddr_in(#include<netinet/in.h>或#include <arpa/inet.h>)类型的结构体指针,它与sockaddr(#include <sys/socket.h>)类型结构体指针互相兼容,但更方便编程,可参考:sockaddr和sockaddr_in详解
在这里插入图片描述
调用bind可以指定IP地址或端口,可以都指定,也可以都不指定,不同指定的结果如下
在这里插入图片描述

字节转换函数

由于网络字节序和主机字节序的不同(大小端模式),在对sockaddr_in结构体的地址和端口赋值的时候,我们需要使用到字节转换函数:

  • htons()——“Host to Network Short” 主机字节顺序转换为网络字节顺序(对无符号短型进行操作2bytes)
  • htonl()——“Host to Network Long” 主机字节顺序转换为网络字节顺序(对无符号长型进行操作4bytes)
  • ntohs()——“Network to Host Short” 网络字节顺序转换为主机字节顺序(对无符号短型进行操作2bytes)
  • ntohl()——“Network to Host Long ” 网络字节顺序转换为主机字节顺序(对无符号长型进行操作4bytes)

对于IPv4来说,通配地址由常值INADDR_ANY来指定,其值一般为0,将地址赋值为INADDR_ANY也即告知由内核去选择IP地址(虽然它值为0,但由于其定义在#include <netinet/in.h>中,而所有INADDR_常值都是按主机字节序定义的,所以我们也习惯性地加上htonl)

地址格式转换函数

  • inet_addr()作用是将一个IP字符串转化为一个网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr。
  • inet_ntoa()作用是将一个sin_addr结构体输出成IP字符串(network to ascii)。比如:printf("%s",inet_ntoa(mysock.sin_addr));

因此,若需要进程指定地址:
ser_addr.sin_addr.s_addr = inet_addr("192.168.1.0");

栗子(内核指定地址,进程指定端口):

	struct sockaddr_in ser_addr;
	int addr_len = sizeof(ser_addr) ;
	ser_addr.sin_family =AF_INET;//地址的协议家族是ipv4协议族
	
	//使用字节转换函数htons主机字节顺序转换为网络字节顺序
	ser_addr.sin_port = htons(8888);//注意,linux操作系统预留1-1024的端口号,所以我们不要使用这一部分
	ser_addr.sin_addr.s_addr =  htonl(INADDR_ANY);//INADDR_ANY自动绑定ip地址(192.168.xxx)
	if(bind(server_socket_fd, (struct sockaddr*)&ser_addr, addr_len)<0)//将sockaddr_in类型转换为sockaddr类型
	{
		perror("bind err:");
		return -1;
	}

listen函数(服务端)

为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求。Linux系统可能会对队列中可以容纳的未处理连接的最大数量做限制。为了遵守这个最大值限制,listen函数将队列长度设置为backlog参数的值。在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字,再往后的连接将被拒绝,导致客户的连接请求失败。
listen函数提供的这种机制允许当服务器繁忙时将后续的客户连接放入队列等待处理。

man:

The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queueis full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds.
backlog参数定义sockfd挂起连接队列可能增长的最大长度。如果连接请求到达队列时
如果已满,客户端可能会收到一个指示econnrefuse的错误,或者,如果基础协议支持重传,则可能忽略请求,以便稍后的重试连接成功。

历史上backlog参数常用的值是5,现代服务器应该是比这大得多的值
listen仅由TCP服务器调用,应该位于调用socket、bind函数之后,调用accept之前调用。
功能:
1、把一个未连接的套接字转换成一个被动套接字(从CLOSED状态转换成LISTEN状态),指示内核应该接受指向该套接字的连接请求
2、指定内核应该为响应套接字排队的最大连接数

#include <sys/socket.h>
int listen(int socket, int backlog);

返回值:
成功0失败-1

栗子:

	if(listen(server_socket_fd, 10)<0)
	{
		perror("listen err:");
		return -1;
	}

accept函数(服务端)

功能:
由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成(接受)的连接。
如果已完成连接队列为空,那么进程被投入睡眠(假设套接字为默认的阻塞方式)
人话:我们在这里称sockfd——即由socket创建,随后用作bind和listen的第一个参数的文件描述符为监听套接字描述符,而称accept函数的返回的套接字描述符为已连接套接字描述符。这两个描述符是不同的。一个服务器通常仅创建一个监听套接字,它在该服务器的生命周期内一直存在。而多个客户监听套接字在连接队列中排队,当服务器accpet时,内核会为每一个被服务器接受(accept)的客户连接创建一个已连接套接字(也就是说对于它的TCP三路握手过程已经完成)当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。

人人话:监听套接字描述符——一直存在,负责引路到酒店门口(accept函数)的礼仪
已连接套接字描述符——给住户搬运行李的服务员(酒店accept才产生),服务完之后就离开(关闭)

       #include <sys/socket.h>
       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

返回值:
成功为非负的,accepted的描述符代表与返回客户的TCP连接,失败为-1
1:socket
前面socket函数返回的,与套接字关联的文件描述符
2:addr
返回已连接的客户进程的协议地址,将其保存在这个结构体里面
3:addrlen
是值——结果参数,《UNP1》3.3 P62说明到:

把套接字地址结构大小这一参数从整数改为指向某个整数变量的指针,其原因在于当函数被调用时,其结果大小是一个值(value)。他告诉内核该结构的大小。这样内核在写该结构时不至于越界;当函数返回时,结构大小又是一个结果(result),他告诉进程内核在该结构中究竟存储了多少信息。这种类型的参数称为值——结果(value-result)参数。

因此:
调用前,我们将由addrlen所引用(指向)的整数值置为addr所指向的结构体大小,返回时,addrlen又会被置为由内核存放在该套接字地址结构内的确切字节数。
当然,如果我们对返回的客户协议地址和大小不敢兴趣,我们通常将其置为NULL
栗子:

//================================accept
	int accept_fd;
	while (1)
	{
		accept_fd = accept(server_socket_fd, NULL, NULL);
	//==============================read,仅服务器从客户端不断接收数据
		char buffer[50] = { 0 };	
		int r_size = 0;
		cout << "server start to read..." << endl;
		pid_t pid;
		pid = fork();
		if (pid == 0)
		{
			while (1)
			{
				cout << "ready to read" << endl;
				r_size = read(accept_fd, buffer, sizeof(buffer));//阻塞这里等待从客户机接收到数据
				//write
				cout << "server receive:" << buffer << endl;
				memset(buffer, 0, sizeof(buffer));

			}
		}
		close(accept_fd);
	}

connect函数(客户端)

功能:
TCP客户端套接字使用该函数建立与TCP服务器套接字的连接
如果需要的话而没有bind的话,内核会确定源IP地址,并选择一个临时端口作为源端口
如果是TCP套接字,调用connect将会激发TCP的三路握手过程
返回值:
成功0失败-1

       #include <sys/socket.h>
       int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

1:sockfd
要连接的,由socket返回的,客户端套接字描述符
2:addr
指向套接字地址结构的指针,含有地址和端口号(可赋值为0交由内核选择)
3:addrlen
套接字地址结构的大小

write函数

函数原型:int write(int fd,void *buf,size_t nbytes);
函数参数:
– fd :要写入的文件的文件描述符
– buf: 指向内存块的指针,从这个内存块中读取数据写入 到文件中
– nbytes: 要写入文件的字节个数
返回值
如果出现错误,返回-1
如果写入成功,则返回写入到文件中的字节个数

read函数

函数原型:
int read(int fd, void *buf, size_t nbytes);
参数
– fd :想要读的文件的文件描述符
– buf: 指向内存块的指针,从文件中读取来的字节放到这个内存块中
– nbytes: 从该文件复制到buf中的字节个数
返回值
如果出现错误,返回-1
返回从该文件复制到规定的缓冲区中的字节数,文件结束,返回0

close函数

功能:
关闭套接字,终止TCP连接

       #include <unistd.h>
       int close(int sockfd);

返回值:
成功0失败-1

getsockname和getpeername函数

       #include <sys/socket.h>
       int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
       int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能:
getsockname返回某个套接字关联的本地协议地址
getpeername返回某个套接字关联的外地址协议
返回值:
成功0失败-1

代码实例

实现功能:实现一个tcp服务器和N个tcp客户端,每个客户端发送字符串数据给服务器,服务器接收并将字符串后面增加client,hello!再发回给对应客户端显示。

服务端代码

#include <iostream>
#include<unistd.h>//unix stand lib
#include<sys/types.h>
#include<sys/fcntl.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<dirent.h>//file dir
#include <sys/wait.h>//wait func
#include <stdlib.h>//ststem
#include <signal.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#include <sys/socket.h>//socket
#include <netinet/in.h>
using namespace std;

int main(int argc, char *argv[])
{
	//==========================================socket
	int server_socket_fd;
	server_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (server_socket_fd < 0)
	{
		perror("create socket err:");
		return -1;
	}
	//==========================================bind
	//socket和socketaddr_in是兼容的,socketaddr_in程序员友好,包含头文件#include <netinet/in.h>
	struct sockaddr_in ser_addr;

	int addr_len = sizeof(ser_addr);
	ser_addr.sin_family = AF_INET;//地址的协议家族是ipv4协议族
	
	//使用字节转换函数htons主机字节顺序转换为网络字节顺序
	ser_addr.sin_port = htons(8888);//注意,linux操作系统预留1-1024的端口号,所以我们不要使用这一部分
	ser_addr.sin_addr.s_addr =  htonl(INADDR_ANY);//INADDR_ANY自动绑定ip地址(192.168.xxx)
	if (bind(server_socket_fd, (struct sockaddr*)&ser_addr, addr_len) < 0)//将sockaddr_in类型转换为sockaddr类型
	{
		perror("bind err:");
		return -1;
	}
	//=================================listen
	if (listen(server_socket_fd, 10) < 0)//参数2表示同一时间能否支持几个客户端同时连接
	{
		perror("listen err:");
		return -1;
	}
	//================================accept
	int accept_fd;
	while (1)
	{
		//当有一个客户端连接到服务器的时候,accept会返回一个新的套接字描述符
		accept_fd = accept(server_socket_fd, NULL, NULL);

	//==============================read
		char buffer[50] = { 0 };	
		int r_size = 0;
		cout << "server start to read..." << endl;
		pid_t pid;
		pid = fork();
		if (pid == 0)
		{
			while (1)
			{
				//从客户端读
				//cout << "server ready to read" << endl;
				r_size = read(accept_fd, buffer, sizeof(buffer));//阻塞这里等待从客户机接收到数据,read字符串会增加\n
				if (r_size > 0)
				{
					cout << "server receive:"<<buffer << endl;
				}
				//再发回客户端
				strcat(buffer, "client,hello!");
				r_size = write(accept_fd, buffer, strlen(buffer));//注意,accept_fd才是向客户写
				if (r_size > 0)
				{
					cout << "server write to client" << endl;
				}
				memset(buffer, 0, sizeof(buffer));

			}
		}
		close(accept_fd);
	}
	close(server_socket_fd);


	
	return 0;
}

客户端代码

#include <iostream>
#include<unistd.h>//unix stand lib
#include<sys/types.h>
#include<sys/fcntl.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<dirent.h>//file dir
#include <sys/wait.h>//wait func
#include <stdlib.h>//ststem
#include <signal.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#include <sys/socket.h>//socket
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

int main(int argc, char *argv[])
{
	//==========================================socket
	int socket_fd;
	struct sockaddr_in ser_addr;
	socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (socket_fd < 0)
	{
		perror("create socket err:");
		return -1;
	}
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_port = htons(8888);
	ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//该函数把ip转换为网络语言#include <arpa/inet.h>
	//或者127.0.0.1表示本机
	//======================================connect
	int addr_len = sizeof(ser_addr);
	if (connect(socket_fd, (struct sockaddr*)&ser_addr, addr_len) < 0)
	{
		perror("connect err:");
		return -1;
	}
	char buffer[50] = { 0 };
	int r_size = 0;
	//不断获取从键盘的输入
	while (fgets(buffer, sizeof(buffer), stdin) != NULL)//阻塞这里等待用户输入
	{
		//客户端发送给服务器,字符串strlen结构体sizeof
		r_size = write(socket_fd, buffer, strlen(buffer));
		if (r_size > 0)
		{
			cout << "client write to server" << endl;
		}
		
		memset(buffer, 0, sizeof(buffer));//清buffer一定是整个buffer大小		
		//客户端从服务器接收
		r_size = read(socket_fd, buffer, sizeof(buffer));//阻塞这里
		if (r_size > 0)
		{
			cout << "client received:" << buffer << endl;
		}
		else
		{
			cout << "client read fail" << endl;
		}
	}
	return 0;
}

最终效果:
我们开两个终端都给服务器发消息并接受:strcat会有个换行符使得client,hello被打印到下一行,但总体结果没错!
在这里插入图片描述
end
参考:《UNP1》第四章
基本TCP套接字编程
TCP套接字编程入门

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值