Linux socket编程入门及客户端服务器端通信实现 – 基础篇

摘要



随着社会网络化的发展,互联网对人们的生活方式产生极大的影响,同时,也创造了一批互联网企业,如著名的BAT。作为一个IT程序员,学会网络通信编程显得十分重要,本文将详细讲解网络编程API之一的套接字编程基本知识,同时充分利用Linux环境下的 Shell脚本和Makefile文件功能,实现一个简单智能化的安装配置。


程序安装包下载

         资源下载


套接字地址结构

----------------------------------------------------------------------------------套接字地址结构-------------------------------------------------------------------------------------------------

一些概念

端口16位整数,TCPUDPSCTP用于区分不同的进程,分为众所周知的端口(1-1023),已登记的端口(1024-49151)和临时端口(49152-65535

套接字标识每个端点的两个值(IP地址和端口号),通常称为一个套接字。

TCP套接字对:定义连接的两个端点四元组:本地地址IP,本地TCP端口号,外地IP地址,外地TCP端口号。套接字对唯一标识一个网络上的每个TCP连接


套接字地址结构定义在 <netinet/in.h> 头文件中

IPv4 结构:

struct  in_addr {
         in_addr_t  s_addr; // 32-bit IPv4 address, 网络字节序
};

struct  sockaddr_in {
         uint8_t                        sin_len;   // 结构长度(16B),若无此项则 协议族为2B
         sa_family_t               sin_family;// AF_INET
         in_port_t                    sin_port;  //16-bit 端口号
         struct  in_addr       sin_addr;  //32- bit IPv4 地址
         char                             sin_zeros[8];// 未使用

}

通用套接字地址结构(<sys/socket.h>)用于处理不同协议族的地址结构引用传递:

struct  sockaddr {
         uint8_t              sa_len;
         sa_family_t      sa_family; // 地址族:AF_XXX value
         char                    sa_data[14];
};


IPv6 结构:

struct  in6_addr {
         in_addr_t  s6_addr[16]; // 128-bit IPv6 address, 网络字节序
};

#define SIN6_LEN 

struct  sockaddr_in6 {
         uint8_t                        sin6_len;   // 结构长度(28B)
         sa_family_t               sin6_family;// AF_INET6
         in_port_t                    sin6_port;  //16-bit 端口号
	 uint32_t                     sin6_flowinfo; //未定义
         struct  in6_addr     sin6_addr;  //128- bit IPv4 
         uint32_t                               sin6_scope_id;// 范围地址

}

IPv6通用套接字地址结构(<netinet/in.h>)可容纳系统支持的任何套接字地址结构:

struct  sockaddr_storage {
         uint8_t              sa_len;
         sa_family_t      sa_family; // 地址族:AF_XXX value

         /*
         …
         */

};


 

基本套接字函数

--------------------------------------------------------------------------基本套接字函数使用介绍---------------------------------------------------------------------------------------


基本的TCP套接字编程函数包括 socketconnectbindlistenaccept5个函数,需包含头文件 #include <sys/socket.h>还有提供服务器并发功能的函数 fork exec函数族,和close函数(用于关闭套接字),包含头文件 #include <unistd.h>

1     socket函数

函数说明:执行网络I/O,一个进程必须做的一件事即调用socket函数,指定期望的通信协议类型

函数定义int  socket(int family, int type, int protocol);

         三个参数分别为协议族,套接字类型,协议类型常值(通常设置为0

IPv4协议用法sockfd =socket(AF_INET,SOCK_STREAM,0);

成功则返回非负描述符,即套接字描述符 sockfd,出错返回 -1


2    connect函数

函数说明基于套接字描述符sockfd的函数,用来建立TCP服务器的连接,调用将激发TCP的三路握手过程,仅在连接建立成功或出错时才返回

函数定义int  connect(int sockfd, conststruct sockaddr *servaddr,socklen_t addrlen);

         套接字描述符,套接字地址结构指针和该结构大小

IPv4协议用法if(connect(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr)) <0) exit(1);

成功返回0,出错返回-1

 

3    bind函数

函数说明:基于套接字描述符sockfd的函数,用来把一个本地协议地址赋予一个套接字,如果不调用bind函数,内核则为相应的套接字选择一个临时端口,但对于服务器来说,需要用bind绑定一个众所周知的端口,这是很必要的。

函数定义int  bind(intsockfd, const struct sockaddr *myaddr, socklen_t addrlen);

         套接字描述符,指向特定于协议的地址结构指针和该地址结构长度

IPv4协议用法if(bind(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr))<0) exit(1);

成功返回0,出错返回-1


4    listen函数

函数说明仅由TCP服务器调用,在服务器端,socket创建一个套接字时,被假设为一个主动套接字,即一个将调用connect发起连接客户套接字,listen函数把一个未连接的套接字转换成一个被动套接字(等待客户请求),指示内核应接受指向该套接字的连接请求。通常在调用socketbind函数之后,并在调用accept函数之前调用。

函数定义int   listen(int sockfd, int backlog) ;

         第二个参数规定了内核应该为相应套接字排队的最大连接个数。监听套接字维护两个队列:未完成连接队列SYN_RCVD)和已完成连接队列ESTABLISHED),未完成连接队列成员经过三路握手成功后可进入已完成连接队列。

IPv4协议用法if(listen(sockfd,LISTEN))exit(1);

成功返回0,出错返回-1


5    accept函数

函数说明:由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接,如果已完成连接队列为空,则进程被投入睡眠状态(阻塞)

函数定义int   accpt(intsockfd, struct sockaddr *cliaddr,socklen_t *addrlen);

         第一个参数即套接字描述符称为监听套接字(一般用listenfd代替sockfd,剩下俩个为已连接的对端客户进程的协议地址和长度,第三个参数为-结果参数

IPv4协议用法connfd=accept(listenfd, (struct sockaddr*) &cliaddr, &len);;

若成功返回非负描述符,称之为已连接套接字(常用connfd表示),出错则返回-1


-监听套接字和已连接套接字的区别:一个服务器通常仅创建一个监听套接字,并且在服务器周期内一直存在,内核为每个由服务器进程接受的客户连接创建一个已连接套接字(即TCP三路握手过程已经完成),当服务器完成对给定客户服务时,相应的已连接套接字关闭。


6    fork函数和exec函数

函数说明:调用一次,返回两次,即父进程和子进程,根据返回值判断当前进程是父进程还是子进程。exec函数通常被fork子进程调用,然后将子进程替换成新的程序,被称为调用进程,具体可参见我之前总结的一篇对这两个函数分析的文章

http://blog.csdn.net/gujinjinseu/article/details/25838381


函数定义pid_t   fork(void);


7   close函数

函数说明用于关闭套接字,并终止TCP连接

函数定义int   close(int sockfd);

成功返回0,出错返回-1

注: 与shutdown 函数的区别,close 关闭可能是多连接的套接字,此时并不关闭fd,而shutdown则直接关闭fd (file descriptor)



------------------------------------------------------------------------------------TCP客户/服务器程序示例-----------------------------------------------------------------------------

TCP服务器端程序

/* ==========================================================
			服务器端程序
   ==========================================================
   By	gujj	20140530
*/
   
#include <stdio.h>		// 标准输入输出流
#include <netinet/in.h> // socket struct
#include <sys/socket.h>	
#include <time.h>		// time
#include <string.h>		// htons()
#include <stdlib.h>		// exit()
#include <unistd.h>		//write()

#define MAXLINE 4096
#define LISTENQ 1024	// 监听套接字最大连接数

int main(int argc, char **argv)
{
	// 声明定义监听套接字,已连接套接字
	int listenfd, connfd,n;
	// 被动套接字 
	struct sockaddr_in servaddr;
	char buff[MAXLINE];
	time_t ticks;
	
	/* 监听套接字函数 */
	if(( listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
	{
		printf("socket error! \n");
		exit(1);
	}
	
	/* 初始化服务器端套接字地址结构 */
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* 任意地址通配 */
	servaddr.sin_port = htons(13);	/* daytime 服务 */
	
	/* bind 函数 */
	if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
	{
		printf("bind error! \n");
		exit(1);
	}
	
	/* listen 函数 */
	// 将套接字变成 内核可以接受的形式
	if( listen(listenfd, LISTENQ) < 0 )
	{
		printf( "listen error! \n" );
		exit(1);
	}
	
	/* 循环处理客户请求并反馈 */
	for( ; ; )
	{
		if((connfd = accept(listenfd,(struct sockaddr *)NULL,NULL))<0)
		{
			printf("accept error! \n");
			exit(1);
		}
		
		ticks = time(NULL);
		snprintf(buff, sizeof(buff), "%24s\r\n", ctime(&ticks));
		if((n = write(connfd,buff,strlen(buff)))<0)
		{
			printf("write error! \n");
			exit(1);			
		}
		
		if(close(connfd)<0)
		{
			printf("accept error! \n");
			exit(1);			
		}		
	}
	
}


TCP客户端程序

/* ==========================================================
			客户端程序
   ==========================================================
   By	gujj	20140530
*/
   
#include <stdio.h>		// 标准输入输出流
#include <netinet/in.h> // socket struct
#include <sys/socket.h>	
#include <arpa/inet.h>	// inet_pton()
#include <string.h>		// htons()
#include <stdlib.h>		// exit()
#include <unistd.h>		// read()

#define MAXLINE 4096

int main(int argc, char **argv)
{
	int sockfd,n;
	char recvline[MAXLINE + 1]; // 最后一个结束符
	struct sockaddr_in servaddr;
	
	/* 参数个数判断 */
	if(argc != 2)
	{
		printf("Usage: a.out <IPaddress> \n");
		exit(1);
	}
	
	/* 套接字函数调用 */
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
	{
		printf("socket error! \n");
		exit(1);
	}
	
	/* 初始化服务器端套接字地址结构 */
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(13);
	
	/* inet_pton 点分十进制到网络字节序二进制地址序列转换 */
	// =0 表示输入地址错误, <0 表示出错
	if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
	{
		printf("inet_pton error for %s \n", argv[1]);
		exit(1);
	}
	
	/* connect 函数 */
	if(connect(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))<0)
	{
		printf("connect error! \n");
		exit(1); 
	}
	
	/* read 函数 */
	// n=0 关闭,n>0 读取字节数, <0 失败
	while(( n = read(sockfd, recvline, MAXLINE)) > 0 )
	{
		recvline[n] = 0; // 最后一个字节 添加 NULL
		if(fputs(recvline,stdout) == EOF)
		{
			printf("fputs error \n");
			exit(1);
		}
	}
	if(n<0)
	{
		printf("read error \n");
		exit(1);
	}
	
	exit(0);
	
}

Makefile 文件

#---------------------------------------
# Generate Server and Client for TCP
#---------------------------------------
# By Gu Jinjin

# multi-target
all: client server

client: tcpclient.o
	gcc -o client tcpclient.o

server: tcpserver.o
	gcc -o server tcpserver.o

# compile
tcpclient.o: tcpclient.c
	gcc -c -g tcpclient.c
	
tcpserver.o: tcpserver.c
	gcc -c -g tcpserver.c

# define the pseudo-target
.PHONY : cleanall clean

cleanall: clean
	rm -rf client server

clean:
	rm -rf tcpclient.o tcpserver.o
	


自动化配置Shell脚本

#! /bin/bash

# By Gu Jinjin
# Install client & server for TCP
# Debug or Test function
# Uninstall
# Note: if you edit in NotePad++, please use UTF-8 without BOM style

if [ $# -ne 1 ];then
	echo "\t------------------------------------"
	echo "\tUsage: sh setup <args>"
	echo "\t args are Numbers as follows:"
	echo "\t 1. make, generate executions"
	echo "\t 2. run, make & run"
	echo "\t 3. make clean, rm *.o"
	echo "\t 4. make cleanall, rm files in 1&2"
	echo "\t----------------------"
	echo "\t Exp: sh setup.sh 2 "
	echo "\t----------------------"
	echo "\t------------------------------------"
	exit 1
fi


case $1 in
	
	1)
	make > info.make 2>&1
	;;
	
	2)
	make > info.make 2>&1
	make clean
	path=`echo $PWD`
	sudo $PWD/server &
	#$PWD/client 127.0.0.1
	
	;;
	
	3)
	make clean
	;;
	
	4)
	sudo pkill server
	make cleanall
	rm -rf info.make
	;;
	
	*)
	echo "\tInput Wrong Args, please use command: "
	echo "\t\tsh setup.sh"
	echo "\tfor more Info!"
	;;
	
esac

exit 0


运行示例

编写脚本实现工程的编译连接安装并运行,是Linux系统下的一大特点和优势,本文实现一个简单的智能化脚本,提供基于TCP协议的客户端服务器端运行测试,以及删除选项,具体参见下图说明,这里解释一下: 1 即make为运行make命令编译 client 和server, 2. 即包括1中内容和运行服务器端,在后台运行, 3. 删除编译产生的 .o文件,4. 终止服务器端后台运行,并删除所有文件

Linux socket编程入门及客户端服务器端通信实现 – 基础篇_第1张图片


参考

UNIX网络编程卷1(第三版)

高级Bash脚本编程指南(Appendix Q, 杨春敏,黄毅 译)

Makefile:http://www.chinaunix.net/old_jh/23/408225.html








你可能感兴趣的:(linux,socket,网络编程)