LINUX下的Socket网络编程TCP/IP

Posted 遥远的歌s

tags:

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

网络

学习过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的三路握手。如下图

四次挥手

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发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节
    过程如下图

    下图为一个完整的TCP连接所发生的实际分组交换情况,包含建立连接,数据交换,连接终止三个阶段。

基本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参数指明协议族,它通常是下述的某个常数值:

type参数指明套接字的类型,它通常是下属的某个值:


protocol参数则应当设为如下所示的某个协议类型的长治,或设置为0,从而以选择所给定的family和type组合系统所给的默认值。

socket函数在成功执行后会返回一个小的飞赴整数值,它与文件描述符类似,我们把它称之为套接字描述符,为了得到这个套接字描述符,我们只是指定来协议族和套接字类型,并没有指定本地协议地址或远程协议地址

2.bind函数

bind函数把一个本地协议地址赋予一个套接字
所在头文件:#include<sys/socket.h>
函数体:int bind(int sockfd, const struct sockaddr * myaddr, socklen_t addrlen);
返回:成功则返回0,出错则为-1
上面socket函数返回的套接字描述符,其本质就是一个文件描述符,只不过不同的是他是一个指向网络的描述符,通过一系列指向最终指向了内核的struct sock结构体,如下图:

既然套接字已经创建好了,接下来就是想要让别人连接我,那么我必须公开我的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为这两个队列的大小之和不过,现在已经改为已完成三次握手队列的大小。

当已完成三次握手后,就会调用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结构体的一个指针,最后一个参数则是这个结构体的大小

整个过程如下图:

基于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;


运行结果

开启服务端

开启客户端

进行数据读取和处理

参考:UNIX网络编程卷1:套接字联网API(第3版)

以上是关于LINUX下的Socket网络编程TCP/IP的主要内容,如果未能解决你的问题,请参考以下文章

浅谈TCP/IP网络编程中socket的行为

基于TCP协议下的socket编程

Socket Programming

深入理解Socket下的TCP/IP通信原理及参数优化

Linux之socket套接字编程20160704

Linux C Socket编程原理及简单实例