一. 什么是socket?
- 网络中进程之间如何通信?
首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。
其实TCP/IP协议族已经帮我们解决了这个问题,网络层的 “ip地址” 可以唯一标识网络中的ip地址,而传输层的 “协议+端口” 可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。 - 在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
- 白话Socket 就是“插座”:端口就是插座上的孔,端口不能被其他进程占用。抽象理解 Socket 类似于操作某个IP地址上的某个端口达到点对点通信的目的, 需要绑定到某个具体的进程中和端口中。
- Socket 原本代表 Unix上的原始套接字(RawSocket),用于描述文件的内存镜像。因为Unix系统设计哲学是“一切皆文件”。所以后来的 网络版的进程间通信就被冠名为“文件描述符”(file desciptor)。很多通讯协议的实现和源代码 都能看到 Socket* _fd 这样的变量命名,也正是因为如此。
- 通俗理解:
socket就是网络连接的端点。就像一根网线,一头连到路由器,一头连到电脑,这两端就是socket。
socket编程就是利用一些函数开发网络应用。这些函数都与socket接口有关。socket接口可以在各种系统上使用,比如linux,windows,mac。
从开发的角度看,socket类似文件。这与开发文件读写程序时你打开的文件类似,只不过这是一个socket文件。
socket的结构socket的结构很简单,只有三个元素,协议,端口号,IP地址。
二. 原理是什么?
1. socket中TCP的三次握手建立连接详解
- 我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
①客户端向服务器发送一个SYN J
②服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
③客户端再想服务器发一个确认ACK K+1 - 这个三次握手发生在socket的那几个函数中呢?
从图中可以看出,当客户端调用connect()时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
2. socket中TCP的四次握手释放连接详解
- 上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
三. socket怎么用?
1. 总体思路
总的来说网络程序是由两个部分组成的:客户端和服务器端。它们的建立步骤一般是:
- 服务器端:socket→bind→listen→accept
- 客户端:socket→connect
2. 相关结构体
struct sockaddr_in{
//sin_family一般为AF_INET(AF_INET表示地址族,PF_INET表示协议族)
unsigned short sin_family;
//sin_port是我们要监听的端口号
unsigned short int sin_port;
//sin_addr是Internet地址,设置为INADDR_ANY表示可以和任何的主机通信
struct in_addr sin_addr;
//sin_zero[8]是用来填充的,让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
unsigned char sin_zero[8];
}
//in_addr就是32位IP地址。
struct in_addr {
unsigned long s_addr;
}
//通用的socket地址,具体到Internet socket,用sockaddr_in,二者可以进行类型转换
struct sockaddr {
unsigned short sa_family; /* 地址族, AF_xxx */
char sa_data[14]; /* 14字节的协议地址*/
}
3. 函数 socket
- 原型:int socket(int domain, int type,int protocol);
- domain:说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等)。
AF_UNIX只能够用于单一的Unix 系统进程间通信,而AF_INET是针对Internet的,因而可以允许在远程主机之间通信。
(当我们 man socket时发现 domain可选项是 PF_*而不是AF_*,因为glibc是posix的实现所以用PF代替了AF,不过我们都可以使用的) - type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM等)
SOCK_STREAM表明我们用的是TCP 协议,这样会提供按顺序的、可靠、双向、面向连接的比特流。
SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的、不可靠、无连接的通信。 - protocol:由于我们指定了type,所以这个地方我们一般只要用0来代替就可以了
- socket函数为网络通讯做基本的准备,类似于文件操作中的open。成功时返回文件描述符,失败时返回-1,看errno可知道出错的详细情况。
- 当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
4. 函数bind
- 原型:int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
- sockfd:是由socket调用返回的文件描述符。
- addrlen:是sockaddr结构的长度。
- bind函数将socket函数创建的套接字,绑定到本地的某个地址和端口上。
- 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器。
而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
5. 函数listen
- 原型:int listen(int sockfd,int backlog)
- sockfd:是bind后的文件描述符。
- backlog:设置请求排队的最大长度.当有多个客户端程序和服务端相连时,使用这个表示可以介绍的排队长度.
- listen函数将bind的文件描述符变为监听套接字。
- 如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket;如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
6. 函数connect
- 原型:int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
- sockfd:socket返回的文件描述符
- serv_addr:储存了服务器端的连接信息。其中sin_add是服务端的地址
- addrlen:serv_addr的长度
- connect函数是客户端用来同服务端连接的。成功时返回0,失败时返回-1。
7. 函数accept
- 原型:int accept(int sockfd, struct sockaddr *addr,int *addrlen)
- sockfd:是listen后的文件描述符
- addr,addrlen:是用来给客户端的程序填写的,服务器端只要传递指针就可以了
- accept调用时,服务器端的程序会一直阻塞到有一个客户程序发出了连接。accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了,失败时返回-1。
8. 函数send
- int send( SOCKET s,const char FAR *buf,int len,int flags);
- 不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据
- 客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答
- 该函数的第一个参数指定发送端套接字描述符
- 第二个参数指明一个存放应用程序要发送数据的缓冲区
- 第三个参数指明实际要发送的数据的字节数
- 第四个参数一般置0
9. 函数recv
- int recv( SOCKET s,char FAR *buf,int len,int flags );
- 不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据
- 该函数的第一个参数指定接收端套接字描述符
- 第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据
- 第三个参数指明buf的长度
- 第四个参数一般置0
四. 其他
1. socket 数量限制
- Windows下限制:
①最大TCP连接数
[HKEY_LOCAL_MACHINE \System \CurrentControlSet \Services \Tcpip \Parameters] TcpNumConnections = 0x00fffffe (Default = 16,777,214)
以上注册表信息配置单机的最大允许的TCP连接数,默认为 16M。这个数值看似很大,这个并不是限制最大连接数的唯一条件,还有其他条件会限制到TCP 连接的最大连接数。
②最大动态端口数
TCP客户端和服务器连接时,客户端必须分配一个动态端口,默认情况下这个动态端口的分配范围为 1024-5000 ,也就是说默认情况下,客户端最多可以同时发起3977 个Socket 连接。我们可以修改如下注册表来调整这个动态端口的范围
[HKEY_LOCAL_MACHINE \System \CurrentControlSet \Services \Tcpip \Parameters] MaxUserPort = 5000 (Default = 5000, Max = 65534)
③最大TCB 数量
系统为每个TCP 连接分配一个TCP 控制块(TCP control block or TCB),这个控制块用于缓存TCP连接的一些参数,每个TCB需要分配 0.5 KB的pagepool 和 0.5KB 的Non-pagepool,也就说,每个TCP连接会占用 1KB 的系统内存。
系统的最大TCB数量由如下注册表设置决定
[HKEY_LOCAL_MACHINE \System \CurrentControlSet \Services \Tcpip \Parameters] MaxFreeTcbs = 2000 (Default = RAM dependent, but usual Pro = 1000, Srv=2000)
- Linux下限制:
①端口号限制
Linux socket使用16bit无符号整型表示端口号,最大到65535。关于端口号,有一个经典的误解就是,因为端口号有限,所以一个客户端最多建立65536个socket连接,但实际上并不是这么回事,端口是可以复用的。
②用户进程可打开文件数限制(主要限制)
在进行高并发TCP连接处理时,最高的并发数量都要受到系统对用户单一进程同时可打开文件数量的限制。因为系统为每个TCP连接都要创建一个socket句柄,每个socket句柄同时也是一个文件句柄。
可用命令 ulimit -n查看(测试:1024)。这表示当前用户的每个进程最多允许同时打开1024个文件,这1024个文件中还得除去每个进程必然打开的标准输入,标准输出,标准错误,服务器监听 socket,进程间通讯的unix域socket等文件,那么剩下的可用于客户端socket连接的文件数就只有大概1024-10=1014个左右。也就是说缺省情况下,基于Linux的通讯程序最多允许同时1014个TCP并发连接。
五. 举些栗子
1. Windows 下的socket编程
- Windows下的socket编程和Linux下可能有些不同,例如socket前要初始化等。
//定义文件名
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
//网络相关头文件
#include <winsock2.h>
//预编译指示加入这个lib,ws2_32.lib是winsock2的库文件
#pragma comment(lib, "ws2_32.lib")
#define SERVER_IP "192.168.1.124" //服务器IP地址
#define CLIENT_PORT 2333 //端口号
#define MAXLINE 1024 //每次传输的最大值
//网络传输用参数
SOCKET sock_id;
char buf[MAXLINE];
//客户端视频传输函数
int client_file(void);
int main() {
int ret = -1;
// 初始化Windows Socket
// WSAStartup函数对Winsock服务的初始化
WSADATA s;
if (WSAStartup(MAKEWORD(2, 2), &s) != 0) // 通过连接两个给定的无符号参数,首个参数为低字节
{
printf("Init Windows Socket Failed! Error: %d\n", GetLastError());
getchar();
return -1;
}
while (ret!=0){
ret = client_file();
}
return 0;
}
//传输函数实现
int client_file(void) {
char filename[] = "send.txt";
struct sockaddr_in serv_addr;
int i_ret;
int send_len; //读到的长度
int read_len;//发送的长度
//客户端socket
sock_id = socket(AF_INET, SOCK_STREAM, 0);
if (sock_id == INVALID_SOCKET) {
printf("socket error\n");
return -1;
}
//设置sockaddr_in
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(CLIENT_PORT); //端口字节转换
serv_addr.sin_addr.s_addr = inet_addr(SERVER_IP);//IP地址转换
//客户端connect
i_ret = connect(sock_id, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
if (SOCKET_ERROR == i_ret) {
printf("Connect socket failed\n");
return -1;
}
printf("Socket connected!\n");
//传输文件
FILE *fp;
fp = fopen(filename, "r");
//当文件还没读完的时候一直循环
while ((read_len = fread(buf, sizeof(char), MAXLINE, fp)) > 0) {
send_len = send(sock_id, buf, read_len, 0);
if (send_len < 0) {
printf("Send file failed\n");
}
//bzero(buf, MAXLINE); //no use
}
fclose(fp);
closesocket(sock_id);
return 0;
}
2. Linux下TCP编程
- 这个例子是我一个项目中用到的,通过TCP传输ARM板上采集的视频到局域网内的电脑。分为客户端(ARM板)和服务端(PC)。
- 客户端程序
/**********************************
程序名称:V4.0 YUV240_to_H264_convert
编写者:SJL
实现功能:
1.利用Linux的V4L2接口,从UVC摄像头读取YUV视频数据,并将其转换为YUV420存储。
2.使用X264完成对采集视频的H264压缩编码,方便下面进行传输。
3.使用TCP协议,将视频传输到目标服务器。
开始日期:2018.4.2
完成日期:2018.4.25
**********************************/
//定义文件名
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>//memset函数
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h> //MMAP相关头文件
//网络相关头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//V4L2头文件
#include<linux/videodev2.h>
//x264编码文件
#include <x264.h>
/*******
定义函数、变量、结构体等
*******/
//定义摄像头相关参数
#define VIDEO_WIDTH 640
#define VIDEO_HEIGHT 480
#define VIDEO_FORMAT V4L2_PIX_FMT_YUYV
#define BUFFER_COUNT 4
//定义编码相关参数
#define FPS 25
#define YUV_SIZE (VIDEO_WIDTH * VIDEO_HEIGHT * 3 / 2)
#define SERVER_IP "192.168.1.124" //服务器IP地址
#define CLIENT_PORT 2333 //端口号
#define MAXLINE 1024 //每次传输的最大值
//设置摄像头设备节点和捕捉所得视频文件名
#define UVC_V4L2 "/dev/video4"
#define CAPTURE_FILE "test.yuv"
#define H264_F "test.h264"
#define VIDEO "/home/linaro/arm_client/test.h264" //要传输的视频文件
//网络传输用参数
int sock_id;
char buf[MAXLINE];
typedef struct VideoBuffer {
void *start;
size_t length;
} VideoBuffer;
//YUY2转换为YUV420函数
void YUV422toYUV420(unsigned char *out, const unsigned char *in, unsigned int width, unsigned int height);
//调用x264编码函数
int h264_encoder(void);
//视频录制、转YUV420、H264编码函数
int YUVTOH264(void);
//客户端视频传输函数
int client_video(void);
VideoBuffer framebuf[BUFFER_COUNT];
/*******
主函数
*******/
int main(){
while(1){
YUVTOH264();
client_video();
}
}
/*******
子函数实现
*******/
//客户端视频传输函数实现
int client_video(void){
struct sockaddr_in serv_addr;
int i_ret;
int send_len; //读到的长度
int read_len;//发送的长度
//客户端socket
if ((sock_id = socket(AF_INET,SOCK_STREAM,0)) < 0) {
printf("Create socket failed\n");
}
//设置sockaddr_in
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(CLIENT_PORT); //端口字节转换
serv_addr.sin_addr.s_addr=inet_addr(SERVER_IP);//IP地址转换
//客户端connect
i_ret = connect(sock_id, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr));
if (-1 == i_ret) {
printf("Connect socket failed\n");
return -1;
}
printf("Socket connected!\n");
//传输视频
FILE *fp;
fp=fopen(VIDEO,"r");
//当文件还没读完的时候一直循环
while ((read_len = fread(buf, sizeof(char), MAXLINE, fp)) >0 ) {
send_len = send(sock_id, buf, read_len, 0);
if ( send_len < 0 ){
printf("Send file failed\n");
}
//bzero(buf, MAXLINE); //no use
}
fclose(fp);
close(sock_id);
return 0;
}
//视频录制、转YUV420、H264编码函数实现
int YUVTOH264(void){
int fd;
int i,ret;
//O_RDWR只读打开(V4L2要用阻塞方式打开)
if((fd = open(UVC_V4L2,O_RDWR))<0){
printf("Camera open %s failed",UVC_V4L2);
}
else{
/*应用程序开始******************/
// 设置视频格式
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = VIDEO_WIDTH;
fmt.fmt.pix.height = VIDEO_HEIGHT;
fmt.fmt.pix.pixelformat = VIDEO_FORMAT;
//fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
if (ret < 0) {
printf("Set VIDIOC_S_FMT failed (%d)\n", ret);
return ret;
}
// 获取视频格式
ret = ioctl(fd, VIDIOC_G_FMT, &fmt);
if (ret < 0) {
printf("VIDIOC_G_FMT failed (%d)\n", ret);
return ret;
}
// 显示当前视频格式
printf("Stream Format Informations:\n");
printf(" type: %d\n", fmt.type);
printf(" width: %d\n", fmt.fmt.pix.width);
printf(" height: %d\n", fmt.fmt.pix.height);
char fmtstr[8];
memset(fmtstr, 0, 8);
memcpy(fmtstr, &fmt.fmt.pix.pixelformat, 4);
printf(" pixelformat: %s\n", fmtstr);
printf(" field: %d\n", fmt.fmt.pix.field);
printf(" bytesperline: %d\n", fmt.fmt.pix.bytesperline);
printf(" sizeimage: %d\n", fmt.fmt.pix.sizeimage);
printf(" colorspace: %d\n", fmt.fmt.pix.colorspace);
printf(" priv: %d\n", fmt.fmt.pix.priv);
printf(" raw_date: %s\n", fmt.fmt.raw_data);
// 请求分配内存
struct v4l2_requestbuffers reqbuf;
reqbuf.count = BUFFER_COUNT;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd , VIDIOC_REQBUFS, &reqbuf);
if(ret < 0) {
printf("VIDIOC_REQBUFS failed (%d)\n", ret);
return ret;
}
// 获取空间
VideoBuffer* buffers = calloc( reqbuf.count, sizeof(*buffers) );
struct v4l2_buffer buf;
for (i = 0; i < reqbuf.count; i++) {
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd , VIDIOC_QUERYBUF, &buf);
if(ret < 0) {
printf("VIDIOC_QUERYBUF (%d) failed (%d)\n", i, ret);
return ret;
}
// mmap buffer
framebuf[i].length = buf.length;
framebuf[i].start = (char *) mmap(0, buf.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
if (framebuf[i].start == MAP_FAILED) {
printf("mmap (%d) failed\n", i);
return -1;
}
// Queen buffer
ret = ioctl(fd , VIDIOC_QBUF, &buf);
if (ret < 0) {
printf("VIDIOC_QBUF (%d) failed (%d)\n", i, ret);
return -1;
}
printf("Frame buffer %d: address=0x%x, length=%d\n", i, (unsigned int)framebuf[i].start, framebuf[i].length);
}
// 开始录制
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_STREAMON, &type);
if (ret < 0) {
printf("VIDIOC_STREAMON failed (%d)\n", ret);
return ret;
}
int count=250;//抓取250帧,即250/25=10s
//打开存储文件
FILE *fp = fopen(CAPTURE_FILE, "wb"); //w是写入(创建/清空)
while(count-- > 0){
// 获取一帧
ret = ioctl(fd, VIDIOC_DQBUF, &buf);
if (ret < 0) {
printf("VIDIOC_DQBUF failed (%d)\n", ret);
return ret;
}
//申请内存空间
unsigned char *YUV420;
YUV420=calloc((VIDEO_WIDTH * VIDEO_HEIGHT*3/2),sizeof(unsigned char));
//将一帧转换为YUV420
YUV422toYUV420(YUV420,(unsigned char *)framebuf[buf.index].start,VIDEO_WIDTH,VIDEO_HEIGHT);
//将照片存入文件
if (fp < 0) {
printf("open frame data file failed\n");
return -1;
}
fwrite(YUV420, 1,VIDEO_WIDTH * VIDEO_HEIGHT*3/2, fp);
printf("Capture one frame saved in %s\n", CAPTURE_FILE);
//释放空间
free(YUV420);
// Re-queen buffer
ret = ioctl(fd, VIDIOC_QBUF, &buf);
if (ret < 0) {
printf("VIDIOC_QBUF failed (%d)\n", ret);
return ret;
}
}
fclose(fp);
// 释放资源
for (i=0; i< 4; i++)
{
munmap(framebuf[i].start, framebuf[i].length);
}
/*应用程序结束******************/
}
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd,VIDIOC_STREAMOFF,&type);
close(fd);
//调用YUV转码H264子程序
h264_encoder();
}
//YUY2转换为YUV420函数实现
void YUV422toYUV420(unsigned char *out, const unsigned char *in, unsigned int width, unsigned int height){
unsigned char *y = out;
unsigned char *u = out + width*height;
unsigned char *v = out + width*height + width*height/4;
unsigned int i=0,j=0;
unsigned int base_h;
unsigned int is_y = 1, is_u = 1;
unsigned int y_index = 0, u_index = 0, v_index = 0;
unsigned long yuv422_length = 2 * width * height;
//序列为YU YV YU YV,一个yuv422帧的长度 width * height * 2 个字节
//丢弃偶数行 u v
for(i=0; i<yuv422_length; i+=2){
*(y+y_index) = *(in+i);
y_index++;
}
for(i=0; i<height; i+=2){
base_h = i*width*2;
for(j=base_h+1; j<base_h+width*2; j+=2){
if(is_u){
*(u+u_index) = *(in+j);
u_index++;
is_u = 0;
}
else{
*(v+v_index) = *(in+j);
v_index++;
is_u = 1;
}
}
}
}
//调用x264编码函数实现
int h264_encoder(){
printf("into main successful.\n");
int ret;
int y_size=VIDEO_WIDTH*VIDEO_HEIGHT;//分辨率
int i,j;
//编码50帧,如果设置为0则编码全部帧
int frame_num=250;
int csp=X264_CSP_I420;
//定义要用到的数据
int iNal = 0;
x264_nal_t* pNals = NULL; //存储压缩编码后的码流数据
x264_t* pHandle = NULL; //编码器
x264_picture_t* pPic_in = (x264_picture_t*)malloc(sizeof(x264_picture_t));//输入图像
x264_picture_t* pPic_out = (x264_picture_t*)malloc(sizeof(x264_picture_t));//输出图像
x264_param_t* pParam = (x264_param_t*)malloc(sizeof(x264_param_t));//设置
//打开目标文件
FILE* fp_src = fopen(CAPTURE_FILE, "rb");
FILE* fp_dst = fopen(H264_F, "wb");
printf("fopen successful.\n");
//设置格式中的缺省值
x264_param_default_preset(pParam, "veryfast", "zerolatency");
//主要参数设置
pParam->i_width = VIDEO_WIDTH;
pParam->i_height = VIDEO_HEIGHT;
pParam->i_csp=csp;
pParam->i_fps_num = FPS;
pParam->i_fps_den = 1;
pParam->i_level_idc=30;//编码复杂度
pParam->i_keyint_max = 25;
pParam->b_intra_refresh = 1;
pParam->b_annexb = 1;
printf("x264_param_default_preset successful.\n");
//设置码率
pParam->rc.f_rf_constant = 25; //f_rf_constant是实际质量,越大图像越花,越小越清晰
pParam->rc.f_rf_constant_max = 45;
pParam->rc.i_rc_method=X264_RC_ABR;//平均码率模式
pParam->rc.i_vbv_max_bitrate = 5000;//要设置的最大码率
pParam->rc.i_bitrate=2500;//要设置的码率
//应用设置
x264_param_apply_profile(pParam, "baseline");//baseline\main\high编码
//打开编码器
pHandle = x264_encoder_open(pParam);
//为图像结构体x264_picture_t分配内存
x264_picture_init(pPic_out);
x264_picture_alloc(pPic_in, csp, pParam->i_width, pParam->i_height);
//循环编码
for( i=0;i<frame_num;i++){
fread(pPic_in->img.plane[0],y_size,1,fp_src); //Y
fread(pPic_in->img.plane[1],y_size/4,1,fp_src); //U
fread(pPic_in->img.plane[2],y_size/4,1,fp_src); //V
pPic_in->i_pts = i;
//编码一帧图像
ret = x264_encoder_encode(pHandle, &pNals, &iNal, pPic_in, pPic_out);
if (ret< 0){
printf("x264_encoder_encode Error.\n");
return -1;
}
printf("Succeed encode frame: %d\n",i);
for ( j = 0; j < iNal; j++){
fwrite(pNals[j].p_payload, 1, pNals[j].i_payload, fp_dst);
}
}
i=0;
//冲刷掉编码器
while(1){
ret = x264_encoder_encode(pHandle, &pNals, &iNal, NULL, pPic_out);
if(ret==0){
break;
}
printf("Flush 1 frame.\n");
for (j = 0; j < iNal; ++j){
fwrite(pNals[j].p_payload, 1, pNals[j].i_payload, fp_dst);
}
i++;
}
x264_picture_clean(pPic_in);
x264_encoder_close(pHandle);
pHandle = NULL;
free(pPic_in);
free(pPic_out);
free(pParam);
fclose(fp_src);
fclose(fp_dst);
return 0;
}
- 服务端程序
/**********************************
程序名称:V1.0_V4L2_TCP_server
编写者:SJL
实现功能:
1.不断从客服端接收视频
开始日期:2018.4.27
完成日期:2018.4.30
**********************************/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*******
定义函数、变量、结构体等
*******/
#define MAXLINE 1024
#define SERVER_PORT 2333
#define LOCAL_V "/home/topeet/X86_server/test.h264"
#define V_COPY "/mnt/hgfs/Shared/video.h264"
//文件复制
void CopyFile(char* src,char* des);
/*******
主函数
*******/
int main()
{
while(1){
struct sockaddr_in serv_addr;
struct sockaddr_in clie_addr;
char buf[MAXLINE];
int sock_id;
int link_id;
int recv_len;
int write_leng;
FILE *fp;
int clie_addr_len;
//w为写打开,创建/清空
if ((fp = fopen(LOCAL_V, "w")) == NULL) {
perror("Open file failed\n");
}
if ((sock_id = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Create socket failed\n");
}
//memset(&serv_addr, 0, sizeof(serv_addr));
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET; //IPv4
serv_addr.sin_port = htons(SERVER_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 设置套接字选项避免地址使用错误
int on=1;
if((setsockopt(sock_id,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0)
{
perror("setsockopt failed");
exit(EXIT_FAILURE);
}
if (bind(sock_id, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0 ) {
perror("Bind socket failed\n");
exit(0);
}
if (-1 == listen(sock_id, 10)) {
perror("Listen socket failed\n");
exit(0);
}
clie_addr_len = sizeof(clie_addr);
link_id = accept(sock_id, (struct sockaddr *)&clie_addr, (socklen_t *)&clie_addr_len);
if (-1 == link_id) {
perror("Accept socket failed\n");
exit(0);
}
bzero(buf, MAXLINE);
while ((recv_len = recv(link_id, buf, MAXLINE, 0)) != 0) {
if(recv_len < 0) {
printf("Recieve Data From Server Failed!\n");
break;
}
printf("#");
write_leng = fwrite(buf, sizeof(char), recv_len, fp);
if (write_leng < recv_len) {
printf("Write file failed\n");
break;
}
bzero(buf,MAXLINE);
}
printf("\nFinish Recieve\n");
fclose(fp);
CopyFile(LOCAL_V,V_COPY);
close(link_id);
close(sock_id);
}
return 0;
}
/*******
子函数
*******/
void CopyFile(char* src,char* des)
{
FILE * file1,*file2;
//使用二进制模式打开文件
file1 = fopen(src,"rb"); // rb 表示读
file2 = fopen(des,"wb"); // wb 表示写
if(!file1)
{
printf("文件%s打开失败!",src);
return;
}
char c;
int index = 0;
fseek(file1,0,SEEK_END); //将源文件定位到文件尾
int length = ftell(file1); //获取当前位置,即文件大小(按字节算)
//printf("%d\n",length); //此处可输出字节数,以进行验证
if(!length)
return;
while(!fseek(file1,index,SEEK_SET)) //循环定位文件,向后移动一个字节
{
fread(&c,1,1,file1); //从源文件读取一个字节的内容到 中间变量 c
fwrite(&c,1,1,file2); //将这个字节的内容写入目标文件
if(index == length - 1) //如果已经读到文件尾,则跳出循环
{
break;
}
index++; //往后推进一个字节
}
fclose(file1); //关闭源文件
fclose(file2); //关闭目标文件
}
六. UDP/IP 编程
1.原理
- 前面提到的基本都是TCP/IP的编程,这是因为考虑到实际应用中可能比较少用到UDP,所以就把这部分放在最后面了。而毕竟我是要去面试的人,也不能因为少用就不学。
- 由以上框图可以看出,客户端要发起一次请求,仅仅需要两个步骤(socket和sendto),而服务器端也仅仅需要三个步骤即可接收到来自客户端的消息(socket、bind、recvfrom)。
2. 相关函数
- 服务器:首先调用socket函数创建一个Socket,然后调用bind函数将其与本机地址以及一个本地端口号绑定,接收到一个客户端时,服务器显示该客户端的IP地址,并将字串返回给客户端。
- 客户端的工作流程:首先调用socket函数创建一个Socket,填写服务器地址及端口号。
①sendto
- 原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
- sockfd:正在监听端口的套接口文件描述符,通过socket获得
buf:发送缓冲区,往往是使用者定义的数组,该数组装有要发送的数据
len:发送缓冲区的大小,单位是字节
flags:填0即可
dest_addr:指向接收数据的主机地址信息的结构体,也就是该参数指定数据要发送到哪个主机哪个进程
addrlen:表示第五个参数所指向内容的长度 - 返回值:成功:返回发送成功的数据长度;失败: -1。
②recvfrom
- 原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
- sockfd:正在监听端口的套接口文件描述符,通过socket获得
buf:接收缓冲区,往往是使用者定义的数组,该数组装有接收到的数据
len:接收缓冲区的大小,单位是字节
flags:填0即可
src_addr:指向发送数据的主机地址信息的结构体,也就是我们可以从该参数获取到数据是谁发出的
addrlen:表示第五个参数所指向内容的长度 - 返回值:成功:返回接收成功的数据长度;失败: -1。
3.例子
- server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define SERVER_PORT 8888
#define BUFF_LEN 1024
void handle_udp_msg(int fd)
{
char buf[BUFF_LEN]; //接收缓冲区,1024字节
socklen_t len;
int count;
struct sockaddr_in clent_addr; //clent_addr用于记录发送方的地址信息
while(1)
{
memset(buf, 0, BUFF_LEN);
len = sizeof(clent_addr);
count = recvfrom(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&clent_addr, &len); //recvfrom是拥塞函数,没有数据就一直拥塞
if(count == -1)
{
printf("recieve data fail!\n");
return;
}
printf("client:%s\n",buf); //打印client发过来的信息
memset(buf, 0, BUFF_LEN);
sprintf(buf, "I have recieved %d bytes data!\n", count); //回复client
printf("server:%s\n",buf); //打印自己发送的信息给
sendto(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&clent_addr, len); //发送信息给client,注意使用了clent_addr结构体指针
}
}
/*
server:
socket-->bind-->recvfrom-->sendto-->close
*/
int main(int argc, char* argv[])
{
int server_fd, ret;
struct sockaddr_in ser_addr;
server_fd = socket(AF_INET, SOCK_DGRAM, 0); //AF_INET:IPV4;SOCK_DGRAM:UDP
if(server_fd < 0)
{
printf("create socket fail!\n");
return -1;
}
memset(&ser_addr, 0, sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址,需要进行网络序转换,INADDR_ANY:本地地址
ser_addr.sin_port = htons(SERVER_PORT); //端口号,需要网络序转换
ret = bind(server_fd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
if(ret < 0)
{
printf("socket bind fail!\n");
return -1;
}
handle_udp_msg(server_fd); //处理接收到的数据
close(server_fd);
return 0;
}
- client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define SERVER_PORT 8888
#define BUFF_LEN 512
#define SERVER_IP "172.0.5.182"
void udp_msg_sender(int fd, struct sockaddr* dst)
{
socklen_t len;
struct sockaddr_in src;
while(1)
{
char buf[BUFF_LEN] = "TEST UDP MSG!\n";
len = sizeof(*dst);
printf("client:%s\n",buf); //打印自己发送的信息
sendto(fd, buf, BUFF_LEN, 0, dst, len);
memset(buf, 0, BUFF_LEN);
recvfrom(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&src, &len); //接收来自server的信息
printf("server:%s\n",buf);
sleep(1); //一秒发送一次消息
}
}
/*
client:
socket-->sendto-->revcfrom-->close
*/
int main(int argc, char* argv[])
{
int client_fd;
struct sockaddr_in ser_addr;
client_fd = socket(AF_INET, SOCK_DGRAM, 0);
if(client_fd < 0)
{
printf("create socket fail!\n");
return -1;
}
memset(&ser_addr, 0, sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
//ser_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY); //注意网络序转换
ser_addr.sin_port = htons(SERVER_PORT); //注意网络序转换
udp_msg_sender(client_fd, (struct sockaddr*)&ser_addr);
close(client_fd);
return 0;
}