这篇博客文章主要是通过收集网络上的优秀文章总结出来的比较全面的教程
linux系统下用socket通讯编写网络聊天室(C语言) github源码地址
一、socket介绍
socket数据传输是一种特殊的I/O,也是一种文件描述符(内核利用文件描述符(file descriptor)来访问文件,文件描述符是非负整数,打开现存文件或新建文件时,内核返回一个文件描述符。读写文件也需要使用文件描述符来指定待读文件)。类似打开文件的函数调用 socket()返回整型的socket描述符, 连接建立、数据传输都是通过它实现。
常用的socket类型有两种:流式socket (SOCK_STREAM)和数据报式socket(SOCK_DGRAM)。流式是一种面向连接的socket,针对于面向连接的TCP服务应用;数据报式socket是一种无连接的socket,对应于无连接的UDP服务应用。在这次编写聊天室中用到的是流式的socket。
对于一个功能齐全的Socket,都要包含以下基本结构,其工作过程包含以下步骤:
- socket创建
- socket绑定(bind)
- 连接(connect)
- 接受(accept)
- 数据传输(send、recv)
- 结束传输(close)
注意,在选择端口时,必须小心。每一个端口提供一种特定的服务,只有给出正确的端口,才 能获得相应的服务。0~1023的端口号为系统所保留,例如http服务的端口号为80,telnet服务的端口号为21,ftp服务的端口号为23, 所以我们在选择端口号时,最好选择一个大于1023的数以防止发生冲突。
二、服务器与客户端介绍
- 服务器程序主要负责监听客户端发来的消息
- 客户端需要登录到服务器端才能实现正常的聊天功能。
服务器的主要功能主要如下:
- 在特定的端口上进行监听。等待客户端的连接。
- 用户可以配置服务器端的监听端口
- 向连接的客户端发送登录成功的消息
- 向已经连接到服务器的客户端的用户发送系统消息
客户端的主要功能
- 运行客户端连接到已经开启服务的服务端
- 用户可以向服务器发送消息
- 用户可以接受服务器发送的系统消息
三、socket通讯步骤
1.socket创建
1)函数原型
int socket(int domain, int type, int protocol) (范围,类型,数据交换规则(通常赋值为0))
2)功能:
将套接字和指定端口相连,成功返回0 否则返回-1。
3)参数介绍:
socket:调用成功,返回socket文件描述符;失败返回-1
bind:将套接字和指定的端口相连,成功返回0 否则返回-1
sock_fd: 时调用socket函数返回的socket描述符
perror: 将上一个函数发生错误的原因输出到标准设备(stderr) 参数所指的字符串先被打印出来 后面在加上错误原因字符串 依照errno的值来决定要输出的字符串
exit(): 指明进程退出的返回值,操作系统根据这个值来判断是否正常退出 0是正常 其他情况是不正常
_exit(): 立刻结束目前进程的执行,并把参数返回给父进程,并关闭未关闭的文件
exit(0):正常退出
exit(1): 异常退出
正常退出表示没有错误 异常退出将返回值交给编译器做其他相关对应操作
4)socket描述符介绍:
socket描述符是一个指向内部数据结构的指针,它指向描述符表入口。调用socket函数时,socket执行体将建立一个socket,实际上”建立一个socket”意味着为一个socket数据结构分配存储空间。socket执行体为你管理描述符表。
5)扩展:
type参数指定socket的类型:
SOCK_STREAM 提供有序、可靠、双向及基于连接的字节流
SOCK_DGRAM 支持数据报
SOCK_SEQPACKET 提供有序、可靠、双向及基于连接的数据报通信
SOCK_RAW 提供对原始网络协议的访问
SOCK_RDM 提供可靠的数据报层,但是不保证有序性
2.socket绑定(bind)
1)bind函数原型
int bind(int sock_fd,struct sockaddr *my_addr,int addrlen)
2)功能介绍
将套接字和指定端口相连,成功返回0 否则返回-1
3)参数介绍
my_addr 指向包含有本机IP地址及端口号信息的sockaddr类型指针
addrlen 常被设置为sizeof(struct sockaddr)
struct sockaddr结构类型是用来保存socket信息的:
struct sockaddr {
unsigned short sa_family; /* 地址族,
AF_xxx */ char sa_data[14]; /* 14 字节的协议地址 */ };
sa_family一般为AF_INET,代表Internet(TCP/IP)地址族;
sa_data则包含该socket的IP地址和端口号。
另外还有一种结构类型:
struct sockaddr_in {
short int sin_family; /* 地址族 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[8]; /* 填充0 以保持与struct sockaddr同样大小 */ };
这个结构更方便使用。
sin_zero: 将sockaddr_in结构填充到与struct sockaddr同样的长度,
bzero()或memset()函数: 将其值置为零。
0x00 : 就是16进制 相当于10进制的0
sin_family:协议族 一般用AF_INET表示TCP/IP协议
sin_addr: 联合体 使用多种方式表示IP地址
用一下赋值自动获得本机IP地址和随机获取没有被占用的端口号
my_addr.sin_port=0; // 系统随机选择一个未被使用的端口号
my_addr.sin_addr.s_addr=INADDR_ANY //填入本机IP地址
使用bind函数前需要将sin_port sin_addr 转换成网络字节优先顺序
htonl(): 把32位值从主机字节序转换成网络字节序
htons(): 把16位值从主机字节序转换成网络字节序
ntohl():把32位值从网络字节序转换成主机字节序
ntohs():把16位值从网络字节序转换成主机字节序
sockaddr_in 的指针和指向sockaddr的指针可以相互转换,这意味着如果一个函数所需参数类型是sockaddr时,你可以在函数调用的时候将一个指向 sockaddr_in的指针转换为指向sockaddr的指针;或者相反。
3.连接(connect):
面向连接的客户程序使用connect函数来配置socket并与服务器建立一个TCP连接
1)函数原型
int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen)
2)功能介绍
客户端发送服务请求 成功返回0 否则返回-1
3)参数介绍
serv_addr是包含远端主机IP地址和端口号的指针
addrlen 远端地址结构的长度
4)注意
客户端不需要调用bind() 因为只需要知道目的机器的IP地址 而客户通过哪个端口与服务器建立连接并不需要关心 socket执行体为你的程序自动选择一个未被占用的端口 通知你的程序数据什么时候到端口
connect函数和远端主机直接连接。只有面向连接的客户程序使用socket时才需要将socket与远程主机相连。
无连接协议从不建立直接连接。 面向连接的服务器从不启动一个连接 只是被动在协议端口监听客户的请求
4.监听(listen)
listen函数使socket处于被动的监听模式 并为该socket建立一个输入数据队列,将到达的服务请求保存在此队列中 知道程序处理它们
1)函数原型
int listen(int sock_fd int backlog);
2)功能介绍
等待指定的端口出现客户端连接 成功返回0 失败返回-1
3)参数介绍
sock_fd: 是socket系统调用返回的socket 描述符;
bcaklog: 指定在请求队列中允许的最大请求数, 进入连接请求将再队列中等待accept()它们,如果队列已满 将拒绝连接请求。
Backlog对队列中等待服务的请求的数目进行了限制,大多数系统缺省值为20。如果一个服务请求到来时,输入队列已满,该socket将拒绝连接请求,客户将收到一个出错信息。
5.接受(accept)
让服务器接受客户的连接请求。 在建立好输入队列后 服务器就调用accept函数 然后睡眠等待客户的连接请求
1)函数原型
int accept(int sock_fd , void *addr, int *addrlen);
2)功能介绍
接受客户端服务请求 返回新的套接字描述符 失败返回-1
3)参数介绍
sockaddr_in变量指针 用来存放提出连接请求服务的主机信息(某台主机从某个端口发出该请求)
当socket收到连接请求时, socket执行体将建立一个新的socket 这个新的socket和请求连接进程的地址联系起来 收到服务请求的初始socket仍可以继续在以前的socket上监听,同时在新的socket描述符上进行数据传输操作
6.数据传输
通过send() recv() 进行socket上数据传输
1)函数原型
int send(int sock_fd, const void *msg, int len, int flags);
int recv(int sock_fd,void *buf, int len , unsigned int flags);
2)功能介绍
发送数据 成功返回实际发送的数据的字节数 失败-1
接受数据,成功0 失败-1
3)参数介绍
msg是指向要发送数据的指针,
flags一般置为0
buf是存放接受数据的缓冲区,
len是缓冲的长度
recv() 返回实际接受字节数
4)扩展
sendto()函数原型为:
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。
recvfrom()函数原型为:
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
from是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。Recvfrom()函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno
如果你对数据报socket调用了connect()函数时,你也可以利用send()和recv()进行数据传输,但该socket仍然是数据报socket,并且利用传输层的UDP服务。但在发送或接收数据报时,内核会自动为之加上目地和源地址信息。
7.结束传输(close)
释放socket
1)函数原型
int close(sock_fd)
2)参数介绍
sockfd是需要关闭的socket的描述符。
3)扩展
你也可以调用shutdown()函数来关闭该socket。该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行。如你可以关闭某socket的写操作而允许继续在该socket上接受数据,直至读入所有数据。
int shutdown(int sock_fd,int how);
shutdown在操作成功时返回0,在出现错误时返回-1并置相应errno
四.代码示例
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socockfd=accket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define BUFFER_SIZE 256
#define PORT 4568
char recv_buff[BUFFER_SIZE];
char send_buff[BUFFER_SIZE];
int main(void)
{
char write_flag=0;
char read_flag=1;
int sockfd,newsockfd;
socklen_t addr_len;
struct sockaddr_in ser_addr;
addr_len=sizeof(struct sockaddr_in);
if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)
{
perror("socket");
exit(1);
}
else
{
printf("creat socket success id:%d\n",sockfd);
}
bzero(&ser_addr,sizeof(struct sockaddr_in));
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(PORT);
ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(sockfd,(struct sockaddr *)(&ser_addr),sizeof(struct sockaddr))<0)
{
perror("bind");
exit(2);
}
else
{
printf("bind success.\n");
}
if(listen(sockfd,10)<0)
{
perror("listen");
exit(3);
}
else
{
printf("listening...\n");
}
memset(send_buff,0x00,sizeof(send_buff));
memset(recv_buff,0x00,sizeof(send_buff));
if((newsockfd=accept(sockfd,(struct sockaddr*)(&ser_addr),&addr_len))<0)
{
perror("accept");
exit(4);
}
else
{
printf("connect success. \n");
}
write(newsockfd,"have connect!",strlen("have connect")); //服务器刚连接的客户端同个新的文件描述符发送 "have connect!"消息
while(1)
{
if(strlen(send_buff)==0&&write_flag==1)
{
fgets(send_buff,BUFFER_SIZE,stdin);
if(write(newsockfd,send_buff,strlen(send_buff))<0)
{
perror("write");
exit(3);
}
else
{
printf("Client:\n");
printf(" %s\n",send_buff);
}
read_flag=1;
write_flag=0;
memset(recv_buff,0x00,strlen(recv_buff));
}
else if(strlen(recv_buff)==0&&read_flag==1)
{
if(read(newsockfd,recv_buff,sizeof(recv_buff))<0)
{
perror("read");
exit(4);
}
else if(strlen(recv_buff)>0)
{
printf("Server:\n");
printf(" : %s\n",recv_buff);
memset(send_buff,0x00,strlen(send_buff));
write_flag=1;
read_flag=0;
}
}
}
close(sockfd);
return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//下面两个头文件为本练习重要部分,用于socket创建,绑定,连接等
#include <sys/types.h> //数据类型定义
#include <sys/socket.h> //提供socket函数及数据结构
#include <netinet/in.h> //定义数据结构sockaddr_in
#include <arpa/inet.h>
#include <unistd.h>
#define BUFFER_SIZE 256 //接收与发送的字节数
#define PORT 4568 //宏定义,定义通信端口
//声明接收与发送函数
char recv_buff[BUFFER_SIZE];
char send_buff[BUFFER_SIZE];
int main(void)
{
char write_flag=0;
char read_flag=1;
int sockfd;//套结字
struct sockaddr_in cli_addr; //服务器的IPv4的套结字结构 //定义地址结> 构
if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)//创建套结字,
{
perror("socket");
exit(1);
}
else
{
printf("connect server success.\n");
}
bzero(&cli_addr,sizeof(struct sockaddr_in));
cli_addr.sin_family=AF_INET;
cli_addr.sin_port=htons(PORT);
cli_addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(connect(sockfd,(struct sockaddr*)(&cli_addr),sizeof(struct sockaddr)) <0)
{
perror("connect");
exit(2);
}
else
{
printf("connect server success.\n");
}
memset(send_buff,0x00,sizeof(send_buff));
memset(recv_buff,0x00,sizeof(send_buff));
while(1)
{
if(strlen(send_buff)==0&&write_flag==1)
{
fgets(send_buff,BUFFER_SIZE,stdin);
if(write(sockfd,send_buff,strlen(send_buff))<0)
{
perror("write");
exit(3);
}
else
{
printf("Client:\n");
printf(" %s\n",send_buff);
}
read_flag=1;
write_flag=0;
memset(recv_buff,0x00,strlen(recv_buff));
}
else if(strlen(recv_buff)==0&&read_flag==1)
{
if(read(sockfd,recv_buff,sizeof(recv_buff))<0)
{
perror("read");
exit(4);
}
else if(strlen(recv_buff)>0)
{
printf("server:\n");
printf(" :%s\n",recv_buff);
memset(send_buff,0x00,strlen(send_buff));
write_flag=1;
read_flag=0;
}
}
}
printf("end.\n");
close(sockfd); //关闭连接
return 0;
}
五.界面展示
运行服务器
./server
运行客户端
./client