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

LINUX下的Socket网络编程TCP/IP

程序员文章站 2022-06-30 10:26:38
...

网络

学习过linux后了解到,在linux系统下的网络编程很有意思,在此,先要说一下TCP/IP,它是一个面向连接且可靠的一组协议,并且是全双工通信。它的存在使得网络世界缤纷多彩。linux内核将会提供一系列函数帮助我们完成网络编程。
想要对网络编程有认识,首先要知道其建立连接,数据传送,断开连接等过程。
我们知道目前从低向上公认有5层分别为:物理层,数据链路层,运输层,网络层,应用层。 这里主要讨论运输层即TCP层。

三次握手(三路握手)

我们知道,建立一个TCP连接需要经过三次握手的情形

  1. 服务器必须准备好接受外来的连接。这通常通过调用socket,bindlisten这三个函数来完成(下文会讲到这三个函数),这里称之为被动打开(passive open)。
  2. 客户通过调用connect发起主动打开(active open)。这导致客户TCP发送一个SYN分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始***。通常SYN分节不携带数据,其所在IP数据包只含有一个IP首部,一个TCP首部以及可能有的TCP选项。
  3. 服务器必须确定(ACK)客户的SYN,同时自己也得发送一个SYN分节它含有服务器将在同一连接中发送的数据的初始***。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。
  4. 客户必须确定服务器的SYN

这种交换至少需要3个分组,因此称之为TCP的三路握手。如下图
LINUX下的Socket网络编程TCP/IP

四次挥手

TCP的连接终止则需要4个分节

  1. 某个应用进程首先调用**close,**我们称该端执行主动关闭(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
  2. 接收到这个FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为一个文件结束符( end-of-file)传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
  3. 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
  4. 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
    既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。我们使用限定词“通常”是因为: 某些情形下步骤1的FIN随数据一起发送;另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节
    过程如下图
    LINUX下的Socket网络编程TCP/IP
    下图为一个完整的TCP连接所发生的实际分组交换情况,包含建立连接,数据交换,连接终止三个阶段。
    LINUX下的Socket网络编程TCP/IP

基本TCP套接字编程

这里主要采用C/S体系,即客户/服务器程序。想要执行网络的I/O,则一个进程必须做的第一件事就是调用socket函数,并且指定期望的协议类型(IPv4的TCP,IPv6的UDP,Unix域字节流协议等)。

一、服务端

1.socket函数

用来创建一个监听套接字,用来知道谁要连我
所在头文件:#include<sys/socket.h>
函数体:int socket(int family, int type, int protocol);

返回:成功则返回非负描述符,出错则为-1
其中family参数指明协议族,它通常是下述的某个常数值:
LINUX下的Socket网络编程TCP/IP
type参数指明套接字的类型,它通常是下属的某个值:

LINUX下的Socket网络编程TCP/IP
protocol参数则应当设为如下所示的某个协议类型的长治,或设置为0,从而以选择所给定的family和type组合系统所给的默认值。
LINUX下的Socket网络编程TCP/IP
socket函数在成功执行后会返回一个小的飞赴整数值,它与文件描述符类似,我们把它称之为套接字描述符,为了得到这个套接字描述符,我们只是指定来协议族和套接字类型,并没有指定本地协议地址或远程协议地址

2.bind函数

bind函数把一个本地协议地址赋予一个套接字
所在头文件:#include<sys/socket.h>
函数体:int bind(int sockfd, const struct sockaddr * myaddr, socklen_t addrlen);
返回:成功则返回0,出错则为-1
上面socket函数返回的套接字描述符,其本质就是一个文件描述符,只不过不同的是他是一个指向网络的描述符,通过一系列指向最终指向了内核的struct sock结构体,如下图:
LINUX下的Socket网络编程TCP/IP
既然套接字已经创建好了,接下来就是想要让别人连接我,那么我必须公开我的IP地址以及端口号,那么bind函数就起到了这样的作用,它用来绑定一个IP地址和端口,便于别人来连接我们。
第一个参数:sockfd就是socket函数成功返回的监听套接字的值。
第二个参数:指向struct sockaddr结构体的一个指针struct sockaddr是一个通用结构体,因为内核并不知道我要绑定的是的IPv4的还是IPv6的甚至是绑定别的东西,因此这里设计一个通用的结构体,便于编程者去决定要绑定的东西。例如IPv4的结构体如下

struct sockaddr_in{
	sa_family_t sin_family;/*地址族:AF_INET*/
	u_int16_t sin_port;/*按网络字节序的端口*/
	struct in_addr sin_addr;/*internet地址,即IP地址*/
}
/*internet地址*/
struct in_addr{
	u_int32_t s_addr/*按网络字节序的地址*/
}

第三个参数:addrlen 则是第二个参数的大小。
这里值得一提的是,既然通过网络传输数据,那如果两方的机器的字节序不一样那么传输的数据是不能用的,因此规定网络上传输数据都要用网络字节序,这里系统已经提供好了一套字节序转换的函数:

#include <arpa/inet.h>
//本机字节序转换尾网络字节序
uint32_t htonl(uint32_t hostlong);//32位
uint16_t htons(uint16_t hostshort);//16位
//网络字节序转换尾本机字节序
uint32_t ntohl(uint32_t netlong);//32位
uint16_t ntohs(uint16_t netshort);//16位

h:host n:net l:int s:short
绑定好后,即相当于我已经公布了我的IP地址和端口号,可以让别人来连接我了,但是这里还是不能连接的,要通过listen函数,转换为被动套接字

listen函数

listen函数是将监听套接字转换为被动套接字。被动套接字的目的是用来让别人连接我们。
所在头文件:#include<sys/socket.h>
函数体:int listen(int sockfd, int backlog)
第一个参数:sockfd为监听套接字
重点是第二个参数:backlog
listen函数会创建两个队列,分别是未完成三次握手的队列已完成三次握手的队列,在一些书上说明backlog为这两个队列的大小之和不过,现在已经改为已完成三次握手队列的大小。
LINUX下的Socket网络编程TCP/IP
当已完成三次握手后,就会调用accept函数。

accept函数

accept函数由TCP调用,用域从已完成三次握手连接的队列对头返回下一个已完成的连接。
这里一个较好的理解就是,比如我拨打卖房电话为78888,那么当我拨打通后,会被转接,这样的话另外一个人拨打这个电话的时候依旧是能拨通的,那么78888就相当于一个监听套接字,用来知道都有哪些人给我打电话了,而拨通后的转接,则相当于真正的连接套接字,用来正真和人通话的主机。所以accept函数内部会从新调用一个socket函数创建一个新的套接字,用来真正的连接通话

所在头文件:#include<sys/socket.h>
函数体:int accept(int sockfd , struct sockaddr * cliaddr, socketlen_t addrlen)
返回:成功则返回非负描述符,出错则为-1

accept函数是一个阻塞函数,当连接建立后,内核会给出一个信号唤醒这个阻塞函数,去创建一个新的socket,那就需要知道我的ID地址和端口,所以第二个参数仍然是指向struct sockaddr这个结构体的一个指针第三个参数则是这个结构体的大小。当accept函数调用成功后,则服务端只需等待客户端就连接上来能开始数据传送和处理数据啦!

二、客户端

当服务端已经完成后,则需要创建客户端。
客户端首先同样需要调用socket函数,既然是要连接服务端,则需要一个套接字来进行连接。这里不需要bind函数,就好比我打110,我拿任何一个电话都可以打110,并不规定我打110的号码只能是某一个。
调用完socket函数后,客户端只需要在调用connect函数进行与服务端的连接

connect函数

这个函数是用来和服务端进行连接的。如果是TCP套接字的话,则这个函数将激发TCP的三路握手,并且仅在连接成功或出错时才返回
所在头文件:#include<sys/socket.h>
函数体:int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);
返回:成功则为0,出错则为-1

connect函数的第一个参数是我上面调用socket返回的套接字第二个参数则是服务器的IP地址和端口号的即指向struct sockaddr结构体的一个指针,最后一个参数则是这个结构体的大小

整个过程如下图:
LINUX下的Socket网络编程TCP/IP

基于CentOS7系统下的编译环境

服务端代码

#include <stdio.h>
#include <stdlib.h>                                                                                  
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

//创建监听套接字
int creat_socket(){
  int lfd = 0;
  lfd = socket(AF_INET,SOCK_STREAM,0);
  if(lfd == -1){//如果出错
    perror("socket");
    exit(1);
  }
  printf("监听套接字创建成功\n");
  return lfd;
}
//服务端将客户端发来的数据中所有的小写字母转换为大写字母
 void change(char *p,int size){
  for(int i = 0;i<size;i++){
    if(p[i] >= 'a' && p[i] <= 'z'){
      p[i] = p[i] - 32;
    }
  }
}
int main(){
	int lfd = creat_socket();//确定ip地址和端口
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(6666);
	inet_aton("192.168.66.66",&addr.sin_addr);
	//绑定
	int r = bind(lfd,(struct sockaddr*)&addr,sizeof(addr));
	printf("绑定成功\n");
	if(r == -1){
		perror("bind");
		exit(1);
	}
	//转换为被动套接字
	if((r = listen(lfd,SOMAXCONN)) == -1){
		perror("listen");
		exit(1);
	}
	printf("被动套接字创建成功,等待连接\n");
	int newfd = accept(lfd,NULL,NULL);//进行连接
	printf("有客户端连接上来\n");
	while(1){
		char buf[1024] = {};
		r = read(newfd,buf,1024);
		if(r == 0){
			break;
		}
		printf("%s\n",buf);                                                                              
		change(buf,strlen(buf));
		write(newfd,buf,r);
	}
	close(lfd);
	close(newfd);
	printf("服务器已断开\n");
	return 0;
}

客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(){
  int lfd = socket(AF_INET,SOCK_STREAM,0);//创建套接字
  if(lfd == -1){
    perror("socket");
    exit(1);
  }
  //填入我要连接的IP地址和端口号
  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_port = htons(6666);
  inet_aton("192.168.66.66",&addr.sin_addr);
  
  //与服务端进行连接
  int r = connect(lfd,(struct sockaddr*)&addr,sizeof(addr));
  if(r == -1){
    perror("connect");
    exit(1);
  }
  char buf[1024] = {};//读取缓冲区
  printf("与服务器连接成功\n");
  while(fgets(buf,1024,stdin) != NULL){//从键盘上读取至buf不为空
    write(lfd,buf,strlen(buf));//写入lfd中
    memset(buf,0x00,sizeof(buf));
    r = read(lfd,buf,1024);//再从lfd读取
    if(r == 0){//表示连接断开
      close(lfd);
      break;
    }
    printf("%s\n",buf);
    memset(buf,0x00,sizeof(buf));
  }
  return 0;
}

运行结果

开启服务端
LINUX下的Socket网络编程TCP/IP
开启客户端
LINUX下的Socket网络编程TCP/IP

进行数据读取和处理
LINUX下的Socket网络编程TCP/IP
参考:UNIX网络编程卷1:套接字联网API(第3版)