欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基本TCP套接字编程

程序员文章站 2022-07-14 20:30:43
...

基本TCP套接字编程

下图是TCP套接字客服/服务器程序的流程图:

基本TCP套接字编程

socket函数

#include<sys/socket.h>

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

socket函数创建一个socket文件描述符。

参数说明:

  • family:指明协议族,为以下值之一:
family 说明
AF_INET Ipv4协议
AF_INET6 Ipv6协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字
AF_KEY **套接字
  • type:指明套接字类型,为以下值之一:
type 说明
SOCK_STREAN 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_SEQPACKET 有序分组套接字
SOCK_RAW 原始套接字
  • protocol:指定协议类型,可以为以下值之一。或者设为0,以选择所给定的family和type组合的系统默认值。
protocol 说明
IPPROTO_CP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP SCTP传输协议

返回值:调用成功时返回一个非负整数值。

connect函数

#include<sys/socket.h>

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

TCP客户端使用connect函数来与服务器建立连接。

参数说明:

  • sockfd:由socket函数返回的套接字描述符。
  • servaddr:指向一个套接字地址结构,该结构必须含有服务器的IP地址和端口号。
  • addrlen:套接字结构的长度。

返回值:

返回值 说明
0 连接成功
-1 连接失败

bind函数

#include<sys/socket.h>

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

bind函数把一个本地协议地址赋予一个套接字。对于网际网协议,协议地址是32位IPv4地址或128位IPv6地址与16位TCP或UDP端口号的组合。

参数说明:

  • sockfd:套接字描述符
  • sockaddr:指向特定协议的地址结构的指针
  • addrlen:该地址结构的长度

返回值:

返回值 结果
0 成功
-1 出错

调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。如果一个TCP客户或服务器未曾调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时接口。

套接字地址结构
  1. IPv4套接字地址结构
struct in_addr{
    in_addr_t       s_addr;
};
struct sockaddr_in{
    uint8_t         sin_len;        //结构长度
    sa_family_t     sin_family;     //协议族
    in_port_t       sin_port;       //端口号
    struct in_addr  sin_addr;       //IP地址
    char            sin_zero[8];
}
  1. 通用套接字地址结构

当作为一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式来传递。然而以这样的指针作为参数之一的任何套接字函数必须处理来自所支持的任何协议族的套接字的地址结构。

struct sockaddr{
    uint8_t     sa_len;         //结构长度
    sa_family_t sa_family;      //协议族
    char        sa_data[14];    //地址    
};

listen函数

#include<sys/socket.h>

int listen(int sockfd, int backlog);

socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户端套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。即调用listen函数会导致套接字从CLOSED状态转换到LISTEN状态。

内核为任何一个给定的监听套接字维护两个队列:

  • 未完成连接队列,每个这样的SYN分节对应其中的一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
  • 已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。

基本TCP套接字编程

当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三次握手的第二个分节:服务器的SYN响应,其中捎带对客户SYN和ACK。这一项一直保留在未完成连接队列中,直到三次握手的第三个分节到达或者该项超时。如果三路握手正常完成,该项就从未完成连接队列转移到已完成连接队列的队尾。当进程调用accept时,已完成队列中的队头项将返回给进程。

当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,而不会向客户发送RST。因为队列满是暂时的,一段时间后客户将重发SYN。要是服务器立即响应一个RST,客户的connect调用就会立即返回一个错误,强制应用程序处理这种情况,而不是利用TCP的超时重传机制来处理。另外,客户无法区别响应SYN和RST究竟意味着”该端口没有服务器在监听“,还是意味着”该端口有服务器在监听,不过它的队列满了。“

参数说明:

  • sockfd:要监听的套接字描述符
  • backlog:指定内核应该为相应套接字排队的最大连接个数

返回值:

返回值 结果
0 成功
-1 出错

历来沿用的样例代码总是给出值为5的backlog,因为这是4.2BSD支持的最大值。然而现在,最繁忙的服务器一天要处理几百万个连接,backlog设为5显然不够,所以繁忙的服务器必须指定一个大得多的backlog值,而且较新的内核必须支持较大的backlog值。

accept函数

#include<sys/socket.h>

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

accept函数从已连接队列中取出队头项,内核由此生成一个全新的描述符,称为已连接套接字,并返回给应用程序。

参数说明:

  • sockfd:监听的套接字描述符
  • cliaddr:接受连接的套接字地址结构
  • addrlen:套接字地址结构的长度

返回值:

返回值 说明
>0 新套接字描述符
<0 出错

close函数

#include<unistd.h>

int close(int sockfd);

close函数是一个系统调用,可以用来关闭套接字,并终止TCP连接。

需要注意的是,close一个套接字的默认行为是把该套接字标记成已关闭,只是说该套接字描述符不能再由调用进程使用,而不是发送FIN关闭TCP连接。close套接字之后,TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的正常的TCP连接终止序列。

close关闭某个文件描述符,只是导致相应描述符的引用计数值减1。若其它进程仍在使用这个文件描述符,则引用计数仍大于0,这时close调用并不引发TCP四分组连接终止序列。

代码示例

  1. server端代码:
//tcp_server.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>

int startup(int _port)
{
	int sock=socket(AF_INET,SOCK_STREAM,0);
	if(sock<0)
	{
		perror("socket");
		exit(1);
	}
	
	struct sockaddr_in local;
	local.sin_family=AF_INET;
	local.sin_port=htons(_port);
	local.sin_addr.s_addr=htonl(INADDR_ANY);
	socklen_t len=sizeof(local);
	
	if(bind(sock,(struct sockaddr*)&local,len)<0)
	{
		perror("bind");
		exit(2);
	}
	
	if(listen(sock,5)<0)
	{
		perror("listen");
		exit(3);
	}
	
	return sock;
}

int main(int argc,const char * argv[])
{
	
	if(argc!=2)
	{
		printf("Usage:%s [loacl_port]\n",argv[0]);
		return 1;
	}
	
	int listen_sock=startup(atoi(argv[1]));
	
	struct sockaddr_in remote;
	socklen_t len=sizeof(struct sockaddr_in);
	
	int client;
	int iDataNum;
	
	while(1){
		printf("监听端口:%d\n",atoi(argv[1]));
		client=accept(listen_sock,(struct sockaddr*)&remote,&len);
		if(client<0)
		{
			perror("accept");
			continue;
		}
		
		printf("get a client,ip:%s,port:%d\n",inet_ntoa(remote.sin_addr),ntohs(remote.sin_port));
		//与客户端进行通信
	}
	close(listen_sock);
	return 0;
}
  1. client端代码
//client.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>

int main(int argc,const char * argv[])
{
	if(argc!=3)
	{
		printf("Usage:%s [ip] [port]\n",argv[0]);
		return 0;
	}
	
	int sock=socket(AF_INET,SOCK_STREAM,0);
	if(sock<0)
	{
		perror("socket");
		return 1;
	}
	
	struct sockaddr_in server;
	server.sin_family=AF_INET;
	server.sin_port=htons(atoi(argv[2]));
	server.sin_addr.s_addr=inet_addr(argv[1]);
	socklen_t len=sizeof(struct sockaddr_in);
	
	if(connect(sock,(struct sockaddr*)&server,len)<0)
	{
			perror("connect");
			return 2;
	}
	
	//与服务器通信
	
	close(sock);
	return 0;
}
	

参考书籍:《Unix网络编程卷1:套接字联网API(第三版)》