一、经常使用函数介绍git
int socket(int domain,int type,int protocol); /* domain:AF_INET设为IPV4 type:SOCK_STREAM对应TCP,SOCK_DGRAM对应UDP protocol:设0 返回值:返回一个套接字,失败返回-1 */
int bind(int sockfd,struct sockaddr *my_addr,int addrlen); /* sockfd:由socket()调用返回的须要绑定的套接字 my_addr:sockaddr类型的地址 addrlen:sizeof(sockaddr)。 返回值:成功返回0;失败返回-1 */
struct sockaddr_in { short sin_family; /* 地址类型,TCPIP协议只能填AF_INET */ unsigned short sin_port; /*使用端口号 */ struct in_addr sin_addr; /* 网络地址,如需绑定全部地址,填INADDR_ANY */ unsigned char sin_zero[8]; /* 填0便可 */ };//通常将其强制转换成sockaddr来使用
uint16_t htons(uint16_t hostshort); /* 把系统的16位整数调整为“大端模式” */
int inet_aton(const char *string, struct in_addr *addr); /* 把字符串的IP地址转化为in_addr结构体 */
int connect(int sockfd,struct sockaddr* serv_addr,int addrlen); /* sockfd:链接到的套接字 serv_addr:链接到的服务器地址 addrlen:sizeof(serv_addr) 返回值:失败返回-1 */
int listen(int sockfd,int backlog);//设置服务器监听模式 /* sockfd:须要设置监听的服务器套接字 backlog:进入队列中容许的链接的个数。 返回值:出错返回-1 */
int accept(int sockfd,void *addr,int* addrlen);//接受已经connect并在款冲队列中等待的套接字,队列为空时默认进入阻塞状态,直到有客户端进行connect() /* sockfd:正在监听端口的套接字 addr:用于存储客户的地址结构体 addrlen:sizeof(struct sockaddr_in) 返回值:失败-1 */
int send(int sockfd,const void* msg,int len,int flags);//TCP发送数据 /* sockfd:发送目标的套接字 msg:须要发送数据的头指针 len:数据的字节长度 flags:设为0 返回值:返回实际发送的字节数。注意,返回值可能比须要发送的字节数要少,此时须要再次发送剩下的字节。如失败返回-1 */
int recv(int sockfd,void* buf,int len,unsigned int flags);//接受TCP数据 /* sockfd:是要读取的套接口字 buf:保存数据的内存入口。 len:缓冲区的最大长度。注意,缓冲区不需用完 flags:设为0 返回值:返回实际读取到缓冲区的字节数,若是出错则返回-1。 */
二、服务器端流程github
设置服务器地址安全
//设置一个socket地址结构server_addr,表明服务器internet地址, 端口 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); //把一段内存区的内容所有设置为0 server_addr.sin_family = AF_INET;// server_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY是全0特殊地址,用于含有多IP地址的服务器,表示同时绑定本身的全部地址 server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);//小端数转大端数函数
建立服务器套接字服务器
//建立用于internet的流协议(TCP)socket,用server_socket表明服务器socket int server_socket = socket(AF_INET,SOCK_STREAM,0); if( server_socket < 0){ printf("Create Socket Failed!"); exit(1); }
绑定地址与套接字网络
//把socket和socket地址结构联系起来 if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))){ printf("Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT); exit(1); }
设置服务器为监听状态dom
//server_socket用于监听 if ( listen(server_socket, LENGTH_OF_LISTEN_QUEUE) ){ printf("Server Listen Failed!"); exit(1); }
接受客户端的链接socket
int new_server_socket = accept(server_socket,(struct sockaddr*)&client_addr,&length); if ( new_server_socket < 0) { printf("Server Accept Failed!\n"); break; }
从客户端接收数据tcp
//接收客户端发送来的信息到buffer中 length = recv(new_server_socket,buffer,BUFFER_SIZE,0);//若是对方在一次链接里发送了两次呢? if (length < 0){ printf("Server Recieve Data Failed!\n"); exit(1); } printf("\n%s",buffer);
发送数据到客户端函数
char buffer[BUFFER_SIZE]; bzero(buffer, BUFFER_SIZE); strcpy(buffer,"Hello,World! 从服务器来!"); strcat(buffer,"\n"); //C语言字符串链接 //发送buffer中的字符串到new_server_socket,实际是给客户端 send(new_server_socket,buffer,BUFFER_SIZE,0);
关闭链接,空出端口ui
//关闭与客户端的链接 close(new_server_socket); //关闭监听用的socket close(server_socket);
三、客户端流程
设置客户端地址
//设置一个socket地址结构client_addr,表明客户机internet地址, 端口 struct sockaddr_in client_addr; bzero(&client_addr,sizeof(client_addr)); //把一段内存区的内容所有设置为0 client_addr.sin_family = AF_INET; //internet协议族 client_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY表示自动获取本机地址 client_addr.sin_port = htons(0); //0表示让系统自动分配一个空闲端口
创建客户端的套接字
//建立用于internet的流协议(TCP)socket,用client_socket表明客户机socket int client_socket = socket(AF_INET,SOCK_STREAM,0); if( client_socket < 0) { printf("Create Socket Failed!\n"); exit(1); }
绑定客户端地址与套接字
//把客户机的socket和客户机的socket地址结构联系起来 if( bind(client_socket,(struct sockaddr*)&client_addr,sizeof(client_addr))){ printf("Client Bind Port Failed!\n"); exit(1); }
设置服务器地址结构体
//设置一个socket地址结构server_addr,表明服务器的internet地址, 端口 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); server_addr.sin_family = AF_INET; if(inet_aton(argv[1],&server_addr.sin_addr) == 0){ //服务器的IP地址来自程序的参数,aton字符串IP地址转化为网络地址格式 printf("Server IP Address Error!\n"); exit(1); } server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
创建于服务器的链接
socklen_t server_addr_length = sizeof(server_addr); //向服务器发起链接,链接成功后client_socket表明了客户机和服务器的一个socket链接 if(connect(client_socket,(struct sockaddr*)&server_addr, server_addr_length) < 0) { printf("Can Not Connect To %s!\n",argv[1]); exit(1); }
接收、发送数据
关闭链接
完整代码:https://github.com/iyjhabc/study_examples/blob/master/server.c
编译c程序 gcc client.c -o client
运行客户端 ./client 127.0.0.1
127.0.0.1为保留IP地址,固定指向本地主机。
----------------------------------------------------------------------------------------------------
虽然知道各个函数怎么用,但这几个函数都是要组合起来使用,组合得不对使用就会出错,下面介绍一下如何才是正确组合TCP函数。
两个计算机经过网络链接,说白了经过两样东西来定位,IP和端口。套接字其实就是一个已经链接到某IP和某端口的一条抽象通道。知道这两个概念,下面就容易理解了。
一、服务器
做为服务器,通常来讲不会首先链接别人,而是等别人主动链接它,他被动等待别人的链接。而且做为公共使用的服务器,必须有固定的端口,不然别人怎么知道怎么找到你?
(1)bind().服务器的第一步是把新建的服务器socket套接字bind一个端口。此时此套接字已经跟服务器的IP和端口牢牢联系在一块儿了(但还没链接)。
(2)listen()与accept().此二函数是一块儿使用的,首先把服务器的套接字设为监听状态,而后在循环里面调用accept。它被调用后会阻塞本身所在的线程,直到有客户端connect为止。
(3)recv().当有客户端链接服务器,accept的阻塞被释放,并返回一个已经连向该客户端的套接字。此时服务器就能够经过此套接字,使用recv函数接受来自客户端的数据,并使用send给客户端发送数据。
(4)close().accept返回的套接字使用完成后,必须使用close把它关闭。
总结来讲,服务器只需本身绑定一个端口,等待客户端的链接。它彻底不用管客户端的ip和端口,由于accept返回的套接字已经包含了链接向客户端IP和端口的链接通路。
二、客户端
做为客户端,通常是主动链接服务器,等待服务器的回应。客户端必须知道想要链接的服务器的IP和端口。
(1)connect().首先客户端新建一个套接字,并根据服务器的IP和端口把套接字connect到服务器,造成链接通路。
(2)send().链接成功后,就能够利用该套接字向服务器发送数据。
(3)recvfrom().由于刚才的套接字已经与服务器造成链接,所以也能够用来接收服务器返回的数据。
这里再说明一下,connect造成与服务器链接这个套接字,其实就是服务器端accept返回那个套接字,正由于如此,客户端才能用这个套接字recv服务器返回的消息。至于为何用recvfrom而不是recv,接下来讲。
服务器与客户端握手对话(客发-服收-服发-客收)代码可参考如下:
客户端:https://github.com/iyjhabc/study_examples/blob/master/tcp_data_transport_client.c
服务器:https://github.com/iyjhabc/study_examples/blob/master/tcp_data_transport.c
三、recv与recvfrom
众所周知recv主要用在TCP中,recvfrom主要用在UDP中,但如客户端须要等待服务器返回的消息,再做下一步运算的话,就应使用recvfrom,如上面的例子,客户端等待服务器放置数据完成,再从新发送下一轮数据。缘由是,recv默认是非阻塞的,当客户端发送完后调用recv,服务器还没发送数据过来recv就已经执行完毕,所以接受不到服务器数据。recvfrom默认是阻塞的,它会等待服务器返回的数据再往下运行。
而且recv只能用于TCP,而recvfrom同时用于TCP和UDP。recvfrom参数的地址指针将会记录接到的数据的来源地址。再有一点,recv和recvfrom参数里面的套接字,必须是已经创建链接的,没链接,怎么知道接受谁呢?要监放任意的客户端,应先使用listen和accept。
四、send
通常来讲,使用一次send,便connect一次。如连续使用send,后面send的数据会丢失。为什么?其实联系服务器的原理便知道。客户端connect后,服务器accept并返回一个链接双方的套接字。此时双方用此套接字发送接收数据。但通常来讲服务器recv一次后便会close该套接字。若是此时客户端继续试用send,天然就发送不成功了。
总结就是,必须在套接字连同的条件下才能使用send(也是recv也是),不少状况是你本身没有断开链接,但对方其实已经close了该套接字了,便形成了发送失败。因此,链接一次发送一次数据send一次,是比较安全的作法。